From d68f263131ece9380c6a1abde47da73e7a5dc21f Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 12 Jul 2021 17:57:39 -0700 Subject: [PATCH 001/123] [flutter_plugin_tools] Support format on Windows (#4150) Allows `format` to run successfully on Windows: - Ensures that no calls exceed the command length limit. - Allows specifying a `java` path to make it easier to run without a system Java (e.g., by pointing to the `java` binary in an Android Studio installation). - Adds clear error messages when `java` or `clang-format` is missing since it's very non-obvious what's wrong otherwise. Bumps the version, which I intended to do in the previous PR but apparently didn't push to the PR. --- script/tool/CHANGELOG.md | 3 +- script/tool/lib/src/format_command.dart | 147 ++++++++++-- script/tool/pubspec.yaml | 2 +- script/tool/test/format_command_test.dart | 275 ++++++++++++++++++++-- 4 files changed, 383 insertions(+), 44 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 3f31a4953f6b..9db94dda37da 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,4 +1,4 @@ -## NEXT +## 0.4.0 - Modified the output format of many commands - **Breaking change**: `firebase-test-lab` no longer supports `*_e2e.dart` @@ -10,6 +10,7 @@ - Deprecated `--plugins` in favor of new `--packages`. `--plugins` continues to work for now, but will be removed in the future. - Make `drive-examples` device detection robust against Flutter tool banners. +- `format` is now supported on Windows. ## 0.3.0 diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index 7954fd044ce4..c67fb96d2835 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -7,17 +7,31 @@ import 'dart:io' as io; import 'package:file/file.dart'; import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; import 'package:platform/platform.dart'; -import 'package:quiver/iterables.dart'; import 'common/core.dart'; import 'common/plugin_command.dart'; import 'common/process_runner.dart'; +/// In theory this should be 8191, but in practice that was still resulting in +/// "The input line is too long" errors. This was chosen as a value that worked +/// in practice in testing with flutter/plugins, but may need to be adjusted +/// based on further experience. +@visibleForTesting +const int windowsCommandLineMax = 8000; + +/// This value is picked somewhat arbitrarily based on checking `ARG_MAX` on a +/// macOS and Linux machine. If anyone encounters a lower limit in pratice, it +/// can be lowered accordingly. +@visibleForTesting +const int nonWindowsCommandLineMax = 1000000; + const int _exitClangFormatFailed = 3; const int _exitFlutterFormatFailed = 4; const int _exitJavaFormatFailed = 5; const int _exitGitFailed = 6; +const int _exitDependencyMissing = 7; final Uri _googleFormatterUrl = Uri.https('github.com', '/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'); @@ -32,8 +46,9 @@ class FormatCommand extends PluginCommand { }) : super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addFlag('fail-on-change', hide: true); argParser.addOption('clang-format', - defaultsTo: 'clang-format', - help: 'Path to executable of clang-format.'); + defaultsTo: 'clang-format', help: 'Path to "clang-format" executable.'); + argParser.addOption('java', + defaultsTo: 'java', help: 'Path to "java" executable.'); } @override @@ -52,7 +67,8 @@ class FormatCommand extends PluginCommand { // This class is not based on PackageLoopingCommand because running the // formatters separately for each package is an order of magnitude slower, // due to the startup overhead of the formatters. - final Iterable files = await _getFilteredFilePaths(getFiles()); + final Iterable files = + await _getFilteredFilePaths(getFiles(), relativeTo: packagesDir); await _formatDart(files); await _formatJava(files, googleFormatterPath); await _formatCppAndObjectiveC(files); @@ -112,19 +128,18 @@ class FormatCommand extends PluginCommand { final Iterable clangFiles = _getPathsWithExtensions( files, {'.h', '.m', '.mm', '.cc', '.cpp'}); if (clangFiles.isNotEmpty) { - print('Formatting .cc, .cpp, .h, .m, and .mm files...'); - final Iterable> batches = partition(clangFiles, 100); - int exitCode = 0; - for (final List batch in batches) { - batch.sort(); // For ease of testing; partition changes the order. - exitCode = await processRunner.runAndStream( - getStringArg('clang-format'), - ['-i', '--style=Google', ...batch], - workingDir: packagesDir); - if (exitCode != 0) { - break; - } + final String clangFormat = getStringArg('clang-format'); + if (!await _hasDependency(clangFormat)) { + printError( + 'Unable to run \'clang-format\'. Make sure that it is in your ' + 'path, or provide a full path with --clang-format.'); + throw ToolExit(_exitDependencyMissing); } + + print('Formatting .cc, .cpp, .h, .m, and .mm files...'); + final int exitCode = await _runBatched( + getStringArg('clang-format'), ['-i', '--style=Google'], + files: clangFiles); if (exitCode != 0) { printError( 'Failed to format C, C++, and Objective-C files: exit code $exitCode.'); @@ -138,10 +153,18 @@ class FormatCommand extends PluginCommand { final Iterable javaFiles = _getPathsWithExtensions(files, {'.java'}); if (javaFiles.isNotEmpty) { + final String java = getStringArg('java'); + if (!await _hasDependency(java)) { + printError( + 'Unable to run \'java\'. Make sure that it is in your path, or ' + 'provide a full path with --java.'); + throw ToolExit(_exitDependencyMissing); + } + print('Formatting .java files...'); - final int exitCode = await processRunner.runAndStream('java', - ['-jar', googleFormatterPath, '--replace', ...javaFiles], - workingDir: packagesDir); + final int exitCode = await _runBatched( + java, ['-jar', googleFormatterPath, '--replace'], + files: javaFiles); if (exitCode != 0) { printError('Failed to format Java files: exit code $exitCode.'); throw ToolExit(_exitJavaFormatFailed); @@ -156,9 +179,8 @@ class FormatCommand extends PluginCommand { print('Formatting .dart files...'); // `flutter format` doesn't require the project to actually be a Flutter // project. - final int exitCode = await processRunner.runAndStream( - flutterCommand, ['format', ...dartFiles], - workingDir: packagesDir); + final int exitCode = await _runBatched(flutterCommand, ['format'], + files: dartFiles); if (exitCode != 0) { printError('Failed to format Dart files: exit code $exitCode.'); throw ToolExit(_exitFlutterFormatFailed); @@ -166,7 +188,12 @@ class FormatCommand extends PluginCommand { } } - Future> _getFilteredFilePaths(Stream files) async { + /// Given a stream of [files], returns the paths of any that are not in known + /// locations to ignore, relative to [relativeTo]. + Future> _getFilteredFilePaths( + Stream files, { + required Directory relativeTo, + }) async { // Returns a pattern to check for [directories] as a subset of a file path. RegExp pathFragmentForDirectories(List directories) { String s = path.separator; @@ -177,8 +204,10 @@ class FormatCommand extends PluginCommand { return RegExp('(?:^|$s)${path.joinAll(directories)}$s'); } + final String fromPath = relativeTo.path; + return files - .map((File file) => file.path) + .map((File file) => path.relative(file.path, from: fromPath)) .where((String path) => // Ignore files in build/ directories (e.g., headers of frameworks) // to avoid useless extra work in local repositories. @@ -212,4 +241,74 @@ class FormatCommand extends PluginCommand { return javaFormatterPath; } + + /// Returns true if [command] can be run successfully. + Future _hasDependency(String command) async { + try { + final io.ProcessResult result = + await processRunner.run(command, ['--version']); + if (result.exitCode != 0) { + return false; + } + } on io.ProcessException { + // Thrown when the binary is missing entirely. + return false; + } + return true; + } + + /// Runs [command] on [arguments] on all of the files in [files], batched as + /// necessary to avoid OS command-line length limits. + /// + /// Returns the exit code of the first failure, which stops the run, or 0 + /// on success. + Future _runBatched( + String command, + List arguments, { + required Iterable files, + }) async { + final int commandLineMax = + platform.isWindows ? windowsCommandLineMax : nonWindowsCommandLineMax; + + // Compute the max length of the file argument portion of a batch. + // Add one to each argument's length for the space before it. + final int argumentTotalLength = + arguments.fold(0, (int sum, String arg) => sum + arg.length + 1); + final int batchMaxTotalLength = + commandLineMax - command.length - argumentTotalLength; + + // Run the command in batches. + final List> batches = + _partitionFileList(files, maxStringLength: batchMaxTotalLength); + for (final List batch in batches) { + batch.sort(); // For ease of testing. + final int exitCode = await processRunner.runAndStream( + command, [...arguments, ...batch], + workingDir: packagesDir); + if (exitCode != 0) { + return exitCode; + } + } + return 0; + } + + /// Partitions [files] into batches whose max string length as parameters to + /// a command (including the spaces between them, and between the list and + /// the command itself) is no longer than [maxStringLength]. + List> _partitionFileList(Iterable files, + {required int maxStringLength}) { + final List> batches = >[[]]; + int currentBatchTotalLength = 0; + for (final String file in files) { + final int length = file.length + 1 /* for the space */; + if (currentBatchTotalLength + length > maxStringLength) { + // Start a new batch. + batches.add([]); + currentBatchTotalLength = 0; + } + batches.last.add(file); + currentBatchTotalLength += length; + } + return batches; + } } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 6273fe9bf277..7dadc598d4b4 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.3.0 +version: 0.4.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index fabef31a1b64..4728c3136556 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -19,8 +19,8 @@ void main() { late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; - late p.Context path; late RecordingProcessRunner processRunner; + late FormatCommand analyzeCommand; late CommandRunner runner; late String javaFormatPath; @@ -29,7 +29,7 @@ void main() { mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); - final FormatCommand analyzeCommand = FormatCommand( + analyzeCommand = FormatCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, @@ -37,7 +37,7 @@ void main() { // Create the java formatter file that the command checks for, to avoid // a download. - path = analyzeCommand.path; + final p.Context path = analyzeCommand.path; javaFormatPath = path.join(path.dirname(path.fromUri(mockPlatform.script)), 'google-java-format-1.3-all-deps.jar'); fileSystem.file(javaFormatPath).createSync(recursive: true); @@ -46,13 +46,39 @@ void main() { runner.addCommand(analyzeCommand); }); - List _getAbsolutePaths( + /// Returns a modified version of a list of [relativePaths] that are relative + /// to [package] to instead be relative to [packagesDir]. + List _getPackagesDirRelativePaths( Directory package, List relativePaths) { + final p.Context path = analyzeCommand.path; + final String relativeBase = + path.relative(package.path, from: packagesDir.path); return relativePaths - .map((String relativePath) => path.join(package.path, relativePath)) + .map((String relativePath) => path.join(relativeBase, relativePath)) .toList(); } + /// Returns a list of [count] relative paths to pass to [createFakePlugin] + /// with name [pluginName] such that each path will be 99 characters long + /// relative to [packagesDir]. + /// + /// This is for each of testing batching, since it means each file will + /// consume 100 characters of the batch length. + List _get99CharacterPathExtraFiles(String pluginName, int count) { + final int padding = 99 - + pluginName.length - + 1 - // the path separator after the plugin name + 1 - // the path separator after the padding + 10; // the file name + const int filenameBase = 10000; + + final p.Context path = analyzeCommand.path; + return [ + for (int i = filenameBase; i < filenameBase + count; ++i) + path.join('a' * padding, '$i.dart'), + ]; + } + test('formats .dart files', () async { const List files = [ 'lib/a.dart', @@ -71,8 +97,11 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - 'flutter', - ['format', ..._getAbsolutePaths(pluginDir, files)], + getFlutterCommand(mockPlatform), + [ + 'format', + ..._getPackagesDirRelativePaths(pluginDir, files) + ], packagesDir.path), ])); }); @@ -85,9 +114,8 @@ void main() { ]; createFakePlugin('a_plugin', packagesDir, extraFiles: files); - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess.failing() - ]; + processRunner.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [MockProcess.failing()]; Error? commandError; final List output = await runCapturingPrint( runner, ['format'], errorHandler: (Error e) { @@ -118,19 +146,20 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + const ProcessCall('java', ['--version'], null), ProcessCall( 'java', [ '-jar', javaFormatPath, '--replace', - ..._getAbsolutePaths(pluginDir, files) + ..._getPackagesDirRelativePaths(pluginDir, files) ], packagesDir.path), ])); }); - test('fails if Java formatter fails', () async { + test('fails with a clear message if Java is not in the path', () async { const List files = [ 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', @@ -146,6 +175,33 @@ void main() { commandError = e; }); + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Unable to run \'java\'. Make sure that it is in your path, or ' + 'provide a full path with --java.'), + ])); + }); + + test('fails if Java formatter fails', () async { + const List files = [ + 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', + 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable['java'] = [ + MockProcess.succeeding(), // check for working java + MockProcess.failing(), // format + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['format'], errorHandler: (Error e) { + commandError = e; + }); + expect(commandError, isA()); expect( output, @@ -154,6 +210,35 @@ void main() { ])); }); + test('honors --java flag', () async { + const List files = [ + 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', + 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', + ]; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: files, + ); + + await runCapturingPrint(runner, ['format', '--java=/path/to/java']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall('/path/to/java', ['--version'], null), + ProcessCall( + '/path/to/java', + [ + '-jar', + javaFormatPath, + '--replace', + ..._getPackagesDirRelativePaths(pluginDir, files) + ], + packagesDir.path), + ])); + }); + test('formats c-ish files', () async { const List files = [ 'ios/Classes/Foo.h', @@ -174,18 +259,20 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + const ProcessCall('clang-format', ['--version'], null), ProcessCall( 'clang-format', [ '-i', '--style=Google', - ..._getAbsolutePaths(pluginDir, files) + ..._getPackagesDirRelativePaths(pluginDir, files) ], packagesDir.path), ])); }); - test('fails if clang-format fails', () async { + test('fails with a clear message if clang-format is not in the path', + () async { const List files = [ 'linux/foo_plugin.cc', 'macos/Classes/Foo.h', @@ -201,6 +288,62 @@ void main() { commandError = e; }); + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Unable to run \'clang-format\'. Make sure that it is in your ' + 'path, or provide a full path with --clang-format.'), + ])); + }); + + test('honors --clang-format flag', () async { + const List files = [ + 'windows/foo_plugin.cpp', + ]; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: files, + ); + + await runCapturingPrint( + runner, ['format', '--clang-format=/path/to/clang-format']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall( + '/path/to/clang-format', ['--version'], null), + ProcessCall( + '/path/to/clang-format', + [ + '-i', + '--style=Google', + ..._getPackagesDirRelativePaths(pluginDir, files) + ], + packagesDir.path), + ])); + }); + + test('fails if clang-format fails', () async { + const List files = [ + 'linux/foo_plugin.cc', + 'macos/Classes/Foo.h', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable['clang-format'] = [ + MockProcess.succeeding(), // check for working clang-format + MockProcess.failing(), // format + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['format'], errorHandler: (Error e) { + commandError = e; + }); + expect(commandError, isA()); expect( output, @@ -246,12 +389,15 @@ void main() { [ '-i', '--style=Google', - ..._getAbsolutePaths(pluginDir, clangFiles) + ..._getPackagesDirRelativePaths(pluginDir, clangFiles) ], packagesDir.path), ProcessCall( - 'flutter', - ['format', ..._getAbsolutePaths(pluginDir, dartFiles)], + getFlutterCommand(mockPlatform), + [ + 'format', + ..._getPackagesDirRelativePaths(pluginDir, dartFiles) + ], packagesDir.path), ProcessCall( 'java', @@ -259,7 +405,7 @@ void main() { '-jar', javaFormatPath, '--replace', - ..._getAbsolutePaths(pluginDir, javaFiles) + ..._getPackagesDirRelativePaths(pluginDir, javaFiles) ], packagesDir.path), ])); @@ -348,4 +494,97 @@ void main() { contains('Unable to determine diff.'), ])); }); + + test('Batches moderately long file lists on Windows', () async { + mockPlatform.isWindows = true; + + const String pluginName = 'a_plugin'; + // -1 since the command itself takes some length. + const int batchSize = (windowsCommandLineMax ~/ 100) - 1; + + // Make the file list one file longer than would fit in the batch. + final List batch1 = + _get99CharacterPathExtraFiles(pluginName, batchSize + 1); + final String extraFile = batch1.removeLast(); + + createFakePlugin( + pluginName, + packagesDir, + extraFiles: [...batch1, extraFile], + ); + + await runCapturingPrint(runner, ['format']); + + // Ensure that it was batched... + expect(processRunner.recordedCalls.length, 2); + // ... and that the spillover into the second batch was only one file. + expect( + processRunner.recordedCalls, + contains( + ProcessCall( + getFlutterCommand(mockPlatform), + [ + 'format', + '$pluginName\\$extraFile', + ], + packagesDir.path), + )); + }); + + // Validates that the Windows limit--which is much lower than the limit on + // other platforms--isn't being used on all platforms, as that would make + // formatting slower on Linux and macOS. + test('Does not batch moderately long file lists on non-Windows', () async { + const String pluginName = 'a_plugin'; + // -1 since the command itself takes some length. + const int batchSize = (windowsCommandLineMax ~/ 100) - 1; + + // Make the file list one file longer than would fit in a Windows batch. + final List batch = + _get99CharacterPathExtraFiles(pluginName, batchSize + 1); + + createFakePlugin( + pluginName, + packagesDir, + extraFiles: batch, + ); + + await runCapturingPrint(runner, ['format']); + + expect(processRunner.recordedCalls.length, 1); + }); + + test('Batches extremely long file lists on non-Windows', () async { + const String pluginName = 'a_plugin'; + // -1 since the command itself takes some length. + const int batchSize = (nonWindowsCommandLineMax ~/ 100) - 1; + + // Make the file list one file longer than would fit in the batch. + final List batch1 = + _get99CharacterPathExtraFiles(pluginName, batchSize + 1); + final String extraFile = batch1.removeLast(); + + createFakePlugin( + pluginName, + packagesDir, + extraFiles: [...batch1, extraFile], + ); + + await runCapturingPrint(runner, ['format']); + + // Ensure that it was batched... + expect(processRunner.recordedCalls.length, 2); + // ... and that the spillover into the second batch was only one file. + expect( + processRunner.recordedCalls, + contains( + ProcessCall( + getFlutterCommand(mockPlatform), + [ + 'format', + '$pluginName/$extraFile', + ], + packagesDir.path), + )); + }); } From ce371a6e7caa7302ecff7527d5afec6c4ccb9d46 Mon Sep 17 00:00:00 2001 From: Aneesh Rao Date: Tue, 13 Jul 2021 22:11:03 +0530 Subject: [PATCH 002/123] [webview_flutter] Fix broken keyboard issue link (#3266) --- packages/webview_flutter/CHANGELOG.md | 4 ++++ packages/webview_flutter/README.md | 2 +- packages/webview_flutter/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index f43812d438f8..46f5e045ddd8 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.10 + +* Fix keyboard issues link in the README. + ## 2.0.9 * Add iOS UI integration test target. diff --git a/packages/webview_flutter/README.md b/packages/webview_flutter/README.md index 2bfc312d36ab..9a613f5f7a8e 100644 --- a/packages/webview_flutter/README.md +++ b/packages/webview_flutter/README.md @@ -19,7 +19,7 @@ The WebView is relying on [Platform Views](https://flutter.dev/docs/development/platform-integration/platform-views) to embed the Android’s webview within the Flutter app. By default a Virtual Display based platform view backend is used, this implementation has multiple -[keyboard](https://github.com/flutter/flutter/issues?q=is%3Aopen+label%3Avd-only+label%3A%22p%3A+webview-keyboard%22). +[keyboard issues](https://github.com/flutter/flutter/issues?q=is%3Aopen+label%3Avd-only+label%3A%22p%3A+webview-keyboard%22). When keyboard input is required we recommend using the Hybrid Composition based platform views implementation. Note that on Android versions prior to Android 10 Hybrid Composition has some [performance drawbacks](https://flutter.dev/docs/development/platform-integration/platform-views#performance). diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index 6acee01924a6..4d984beeed96 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.0.9 +version: 2.0.10 environment: sdk: ">=2.12.0 <3.0.0" From fbb4b3a85e3a554ef24b5d19898cdc30537737fa Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 13 Jul 2021 13:25:41 -0700 Subject: [PATCH 003/123] [flutter_plugin_tools] Improve license-check output (#4154) Currently each type of check handles its output in isolation, which creates confusing output when the last check succeeds but an earlier check fails, since the end of the output will just be a success message. This makes the output follow the same basic approach as the package looper commands, where all failures are collected, and then a final summary is presented at the end, so the last message will always reflect the important details. It also adopts the colorized output now used by most other commands. --- script/tool/CHANGELOG.md | 4 + .../tool/lib/src/license_check_command.dart | 122 ++++++++------- .../tool/test/license_check_command_test.dart | 147 +++++++++++++----- 3 files changed, 175 insertions(+), 98 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 9db94dda37da..17b28927538d 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +- Improved `license-check` output. + ## 0.4.0 - Modified the output format of many commands diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 093f8143df4f..e68585c44bdf 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -107,21 +107,65 @@ class LicenseCheckCommand extends PluginCommand { @override Future run() async { - final Iterable codeFiles = (await _getAllFiles()).where((File file) => + final Iterable allFiles = await _getAllFiles(); + + final Iterable codeFiles = allFiles.where((File file) => _codeFileExtensions.contains(p.extension(file.path)) && !_shouldIgnoreFile(file)); - final Iterable firstPartyLicenseFiles = (await _getAllFiles()).where( - (File file) => - path.basename(file.basename) == 'LICENSE' && !_isThirdParty(file)); + final Iterable firstPartyLicenseFiles = allFiles.where((File file) => + path.basename(file.basename) == 'LICENSE' && !_isThirdParty(file)); - final bool copyrightCheckSucceeded = await _checkCodeLicenses(codeFiles); - print('\n=======================================\n'); - final bool licenseCheckSucceeded = + final List licenseFileFailures = await _checkLicenseFiles(firstPartyLicenseFiles); + final Map<_LicenseFailureType, List> codeFileFailures = + await _checkCodeLicenses(codeFiles); + + bool passed = true; + + print('\n=======================================\n'); + + if (licenseFileFailures.isNotEmpty) { + passed = false; + printError( + 'The following LICENSE files do not follow the expected format:'); + for (final File file in licenseFileFailures) { + printError(' ${file.path}'); + } + printError('Please ensure that they use the exact format used in this ' + 'repository".\n'); + } + + if (codeFileFailures[_LicenseFailureType.incorrectFirstParty]!.isNotEmpty) { + passed = false; + printError('The license block for these files is missing or incorrect:'); + for (final File file + in codeFileFailures[_LicenseFailureType.incorrectFirstParty]!) { + printError(' ${file.path}'); + } + printError( + 'If this third-party code, move it to a "third_party/" directory, ' + 'otherwise ensure that you are using the exact copyright and license ' + 'text used by all first-party files in this repository.\n'); + } + + if (codeFileFailures[_LicenseFailureType.unknownThirdParty]!.isNotEmpty) { + passed = false; + printError( + 'No recognized license was found for the following third-party files:'); + for (final File file + in codeFileFailures[_LicenseFailureType.unknownThirdParty]!) { + printError(' ${file.path}'); + } + print('Please check that they have a license at the top of the file. ' + 'If they do, the license check needs to be updated to recognize ' + 'the new third-party license block.\n'); + } - if (!copyrightCheckSucceeded || !licenseCheckSucceeded) { + if (!passed) { throw ToolExit(1); } + + printSuccess('All files passed validation!'); } // Creates the expected copyright+license block for first-party code. @@ -135,9 +179,10 @@ class LicenseCheckCommand extends PluginCommand { '${comment}found in the LICENSE file.$suffix\n'; } - // Checks all license blocks for [codeFiles], returning false if any of them - // fail validation. - Future _checkCodeLicenses(Iterable codeFiles) async { + /// Checks all license blocks for [codeFiles], returning any that fail + /// validation. + Future>> _checkCodeLicenses( + Iterable codeFiles) async { final List incorrectFirstPartyFiles = []; final List unrecognizedThirdPartyFiles = []; @@ -171,7 +216,6 @@ class LicenseCheckCommand extends PluginCommand { } } } - print('\n'); // Sort by path for more usable output. final int Function(File, File) pathCompare = @@ -179,38 +223,14 @@ class LicenseCheckCommand extends PluginCommand { incorrectFirstPartyFiles.sort(pathCompare); unrecognizedThirdPartyFiles.sort(pathCompare); - if (incorrectFirstPartyFiles.isNotEmpty) { - print('The license block for these files is missing or incorrect:'); - for (final File file in incorrectFirstPartyFiles) { - print(' ${file.path}'); - } - print('If this third-party code, move it to a "third_party/" directory, ' - 'otherwise ensure that you are using the exact copyright and license ' - 'text used by all first-party files in this repository.\n'); - } - - if (unrecognizedThirdPartyFiles.isNotEmpty) { - print( - 'No recognized license was found for the following third-party files:'); - for (final File file in unrecognizedThirdPartyFiles) { - print(' ${file.path}'); - } - print('Please check that they have a license at the top of the file. ' - 'If they do, the license check needs to be updated to recognize ' - 'the new third-party license block.\n'); - } - - final bool succeeded = - incorrectFirstPartyFiles.isEmpty && unrecognizedThirdPartyFiles.isEmpty; - if (succeeded) { - print('All source files passed validation!'); - } - return succeeded; + return <_LicenseFailureType, List>{ + _LicenseFailureType.incorrectFirstParty: incorrectFirstPartyFiles, + _LicenseFailureType.unknownThirdParty: unrecognizedThirdPartyFiles, + }; } - // Checks all provide LICENSE files, returning false if any of them - // fail validation. - Future _checkLicenseFiles(Iterable files) async { + /// Checks all provided LICENSE [files], returning any that fail validation. + Future> _checkLicenseFiles(Iterable files) async { final List incorrectLicenseFiles = []; for (final File file in files) { @@ -219,22 +239,8 @@ class LicenseCheckCommand extends PluginCommand { incorrectLicenseFiles.add(file); } } - print('\n'); - if (incorrectLicenseFiles.isNotEmpty) { - print('The following LICENSE files do not follow the expected format:'); - for (final File file in incorrectLicenseFiles) { - print(' ${file.path}'); - } - print( - 'Please ensure that they use the exact format used in this repository".\n'); - } - - final bool succeeded = incorrectLicenseFiles.isEmpty; - if (succeeded) { - print('All LICENSE files passed validation!'); - } - return succeeded; + return incorrectLicenseFiles; } bool _shouldIgnoreFile(File file) { @@ -255,3 +261,5 @@ class LicenseCheckCommand extends PluginCommand { .map((FileSystemEntity file) => file as File) .toList(); } + +enum _LicenseFailureType { incorrectFirstParty, unknownThirdParty } diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 64adc9214d80..288cf4696a59 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -131,8 +131,12 @@ void main() { await runCapturingPrint(runner, ['license-check']); // Sanity check that the test did actually check a file. - expect(output, contains('Checking checked.cc')); - expect(output, contains('All source files passed validation!')); + expect( + output, + containsAllInOrder([ + contains('Checking checked.cc'), + contains('All files passed validation!'), + ])); }); test('handles the comment styles for all supported languages', () async { @@ -150,10 +154,14 @@ void main() { await runCapturingPrint(runner, ['license-check']); // Sanity check that the test did actually check the files. - expect(output, contains('Checking file_a.cc')); - expect(output, contains('Checking file_b.sh')); - expect(output, contains('Checking file_c.html')); - expect(output, contains('All source files passed validation!')); + expect( + output, + containsAllInOrder([ + contains('Checking file_a.cc'), + contains('Checking file_b.sh'), + contains('Checking file_c.html'), + contains('All files passed validation!'), + ])); }); test('fails if any checked files are missing license blocks', () async { @@ -176,12 +184,14 @@ void main() { // Failure should give information about the problematic files. expect( output, - contains( - 'The license block for these files is missing or incorrect:')); - expect(output, contains(' bad.cc')); - expect(output, contains(' bad.h')); + containsAllInOrder([ + contains( + 'The license block for these files is missing or incorrect:'), + contains(' bad.cc'), + contains(' bad.h'), + ])); // Failure shouldn't print the success message. - expect(output, isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains(contains('All files passed validation!')))); }); test('fails if any checked files are missing just the copyright', () async { @@ -202,11 +212,13 @@ void main() { // Failure should give information about the problematic files. expect( output, - contains( - 'The license block for these files is missing or incorrect:')); - expect(output, contains(' bad.cc')); + containsAllInOrder([ + contains( + 'The license block for these files is missing or incorrect:'), + contains(' bad.cc'), + ])); // Failure shouldn't print the success message. - expect(output, isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains(contains('All files passed validation!')))); }); test('fails if any checked files are missing just the license', () async { @@ -227,11 +239,13 @@ void main() { // Failure should give information about the problematic files. expect( output, - contains( - 'The license block for these files is missing or incorrect:')); - expect(output, contains(' bad.cc')); + containsAllInOrder([ + contains( + 'The license block for these files is missing or incorrect:'), + contains(' bad.cc'), + ])); // Failure shouldn't print the success message. - expect(output, isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains(contains('All files passed validation!')))); }); test('fails if any third-party code is not in a third_party directory', @@ -250,11 +264,13 @@ void main() { // Failure should give information about the problematic files. expect( output, - contains( - 'The license block for these files is missing or incorrect:')); - expect(output, contains(' third_party.cc')); + containsAllInOrder([ + contains( + 'The license block for these files is missing or incorrect:'), + contains(' third_party.cc'), + ])); // Failure shouldn't print the success message. - expect(output, isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains(contains('All files passed validation!')))); }); test('succeeds for third-party code in a third_party directory', () async { @@ -276,8 +292,12 @@ void main() { await runCapturingPrint(runner, ['license-check']); // Sanity check that the test did actually check the file. - expect(output, contains('Checking a_plugin/lib/src/third_party/file.cc')); - expect(output, contains('All source files passed validation!')); + expect( + output, + containsAllInOrder([ + contains('Checking a_plugin/lib/src/third_party/file.cc'), + contains('All files passed validation!'), + ])); }); test('allows first-party code in a third_party directory', () async { @@ -294,9 +314,12 @@ void main() { await runCapturingPrint(runner, ['license-check']); // Sanity check that the test did actually check the file. - expect(output, - contains('Checking a_plugin/lib/src/third_party/first_party.cc')); - expect(output, contains('All source files passed validation!')); + expect( + output, + containsAllInOrder([ + contains('Checking a_plugin/lib/src/third_party/first_party.cc'), + contains('All files passed validation!'), + ])); }); test('fails for licenses that the tool does not expect', () async { @@ -320,11 +343,13 @@ void main() { // Failure should give information about the problematic files. expect( output, - contains( - 'No recognized license was found for the following third-party files:')); - expect(output, contains(' third_party/bad.cc')); + containsAllInOrder([ + contains( + 'No recognized license was found for the following third-party files:'), + contains(' third_party/bad.cc'), + ])); // Failure shouldn't print the success message. - expect(output, isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains(contains('All files passed validation!')))); }); test('Apache is not recognized for new authors without validation changes', @@ -353,11 +378,13 @@ void main() { // Failure should give information about the problematic files. expect( output, - contains( - 'No recognized license was found for the following third-party files:')); - expect(output, contains(' third_party/bad.cc')); + containsAllInOrder([ + contains( + 'No recognized license was found for the following third-party files:'), + contains(' third_party/bad.cc'), + ])); // Failure shouldn't print the success message. - expect(output, isNot(contains('All source files passed validation!'))); + expect(output, isNot(contains(contains('All files passed validation!')))); }); test('passes if all first-party LICENSE files are correctly formatted', @@ -370,8 +397,12 @@ void main() { await runCapturingPrint(runner, ['license-check']); // Sanity check that the test did actually check the file. - expect(output, contains('Checking LICENSE')); - expect(output, contains('All LICENSE files passed validation!')); + expect( + output, + containsAllInOrder([ + contains('Checking LICENSE'), + contains('All files passed validation!'), + ])); }); test('fails if any first-party LICENSE files are incorrectly formatted', @@ -387,7 +418,7 @@ void main() { }); expect(commandError, isA()); - expect(output, isNot(contains('All LICENSE files passed validation!'))); + expect(output, isNot(contains(contains('All files passed validation!')))); }); test('ignores third-party LICENSE format', () async { @@ -400,8 +431,42 @@ void main() { await runCapturingPrint(runner, ['license-check']); // The file shouldn't be checked. - expect(output, isNot(contains('Checking third_party/LICENSE'))); - expect(output, contains('All LICENSE files passed validation!')); + expect(output, isNot(contains(contains('Checking third_party/LICENSE')))); + }); + + test('outputs all errors at the end', () async { + root.childFile('bad.cc').createSync(); + root + .childDirectory('third_party') + .childFile('bad.cc') + .createSync(recursive: true); + final File license = root.childFile('LICENSE'); + license.createSync(); + license.writeAsStringSync(_incorrectLicenseFileText); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Checking LICENSE'), + contains('Checking bad.cc'), + contains('Checking third_party/bad.cc'), + contains( + 'The following LICENSE files do not follow the expected format:'), + contains(' LICENSE'), + contains( + 'The license block for these files is missing or incorrect:'), + contains(' bad.cc'), + contains( + 'No recognized license was found for the following third-party files:'), + contains(' third_party/bad.cc'), + ])); }); }); } From cf80430e72e1b7515c50120da137852cda92ee01 Mon Sep 17 00:00:00 2001 From: Yusuf <82844127+ydag@users.noreply.github.com> Date: Wed, 14 Jul 2021 11:26:05 +0200 Subject: [PATCH 004/123] [image_picker] Image picker fix camera device (#3898) --- .../image_picker/image_picker/CHANGELOG.md | 6 +- .../image_picker/example/ios/Podfile | 3 + .../ios/Runner.xcodeproj/project.pbxproj | 2 +- .../ios/RunnerTests/ImagePickerPluginTests.m | 111 ++++++++++++++---- .../ios/Classes/FLTImagePickerPlugin.m | 41 ++++--- .../image_picker/image_picker/pubspec.yaml | 2 +- 6 files changed, 123 insertions(+), 42 deletions(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 0e49912b4ed4..33178dee8999 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,9 +1,13 @@ +## 0.8.1+4 + +* Fixes an issue where `preferredCameraDevice` option is not working for `getVideo` method. +* Refactor unit tests that were device-only before. + ## 0.8.1+3 * Fix image picker causing a crash when the cache directory is deleted. ## 0.8.1+2 - * Update the example app to support the multi-image feature. ## 0.8.1+1 diff --git a/packages/image_picker/image_picker/example/ios/Podfile b/packages/image_picker/image_picker/example/ios/Podfile index 75efae48b439..8979c25fea5e 100644 --- a/packages/image_picker/image_picker/example/ios/Podfile +++ b/packages/image_picker/image_picker/example/ios/Podfile @@ -31,7 +31,10 @@ target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do + platform :ios, '9.0' inherit! :search_paths + # Pods for testing + pod 'OCMock', '~> 3.8.1' end target 'RunnerUITests' do inherit! :search_paths diff --git a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj index 547c2be4f914..fc1609f5eeda 100644 --- a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj @@ -877,7 +877,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = NHAKRD9N7D; + DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RunnerUITestiOS14/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m b/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m index f667526671f7..cc901f084071 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m +++ b/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m @@ -6,6 +6,7 @@ @import image_picker; @import XCTest; +#import @interface MockViewController : UIViewController @property(nonatomic, retain) UIViewController *mockPresented; @@ -27,15 +28,33 @@ - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker; @end @interface ImagePickerPluginTests : XCTestCase +@property(readonly, nonatomic) id mockUIImagePicker; +@property(readonly, nonatomic) id mockAVCaptureDevice; @end @implementation ImagePickerPluginTests -#pragma mark - Test camera devices, no op on simulators +- (void)setUp { + _mockUIImagePicker = OCMClassMock([UIImagePickerController class]); + _mockAVCaptureDevice = OCMClassMock([AVCaptureDevice class]); +} + - (void)testPluginPickImageDeviceBack { - if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { - return; - } + // UIImagePickerControllerSourceTypeCamera is supported + OCMStub(ClassMethod( + [_mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) + .andReturn(YES); + + // UIImagePickerControllerCameraDeviceRear is supported + OCMStub(ClassMethod( + [_mockUIImagePicker isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear])) + .andReturn(YES); + + // AVAuthorizationStatusAuthorized is supported + OCMStub([_mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusAuthorized); + + // Run test FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"pickImage" @@ -43,14 +62,27 @@ - (void)testPluginPickImageDeviceBack { [plugin handleMethodCall:call result:^(id _Nullable r){ }]; + XCTAssertEqual([plugin getImagePickerController].cameraDevice, UIImagePickerControllerCameraDeviceRear); } - (void)testPluginPickImageDeviceFront { - if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { - return; - } + // UIImagePickerControllerSourceTypeCamera is supported + OCMStub(ClassMethod( + [_mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) + .andReturn(YES); + + // UIImagePickerControllerCameraDeviceFront is supported + OCMStub(ClassMethod([_mockUIImagePicker + isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront])) + .andReturn(YES); + + // AVAuthorizationStatusAuthorized is supported + OCMStub([_mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusAuthorized); + + // Run test FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"pickImage" @@ -58,14 +90,27 @@ - (void)testPluginPickImageDeviceFront { [plugin handleMethodCall:call result:^(id _Nullable r){ }]; + XCTAssertEqual([plugin getImagePickerController].cameraDevice, UIImagePickerControllerCameraDeviceFront); } - (void)testPluginPickVideoDeviceBack { - if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { - return; - } + // UIImagePickerControllerSourceTypeCamera is supported + OCMStub(ClassMethod( + [_mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) + .andReturn(YES); + + // UIImagePickerControllerCameraDeviceRear is supported + OCMStub(ClassMethod( + [_mockUIImagePicker isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear])) + .andReturn(YES); + + // AVAuthorizationStatusAuthorized is supported + OCMStub([_mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusAuthorized); + + // Run test FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"pickVideo" @@ -73,44 +118,62 @@ - (void)testPluginPickVideoDeviceBack { [plugin handleMethodCall:call result:^(id _Nullable r){ }]; + XCTAssertEqual([plugin getImagePickerController].cameraDevice, UIImagePickerControllerCameraDeviceRear); } -- (void)testPluginPickImageDeviceCancelClickMultipleTimes { - if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { - return; - } +- (void)testPluginPickVideoDeviceFront { + // UIImagePickerControllerSourceTypeCamera is supported + OCMStub(ClassMethod( + [_mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) + .andReturn(YES); + + // UIImagePickerControllerCameraDeviceFront is supported + OCMStub(ClassMethod([_mockUIImagePicker + isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront])) + .andReturn(YES); + + // AVAuthorizationStatusAuthorized is supported + OCMStub([_mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusAuthorized); + + // Run test FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" + [FlutterMethodCall methodCallWithMethodName:@"pickVideo" arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; [plugin handleMethodCall:call result:^(id _Nullable r){ }]; - plugin.result = ^(id result) { - }; - [plugin imagePickerControllerDidCancel:[plugin getImagePickerController]]; - [plugin imagePickerControllerDidCancel:[plugin getImagePickerController]]; + XCTAssertEqual([plugin getImagePickerController].cameraDevice, + UIImagePickerControllerCameraDeviceFront); } -- (void)testPluginPickVideoDeviceFront { - if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { +#pragma mark - Test camera devices, no op on simulators + +- (void)testPluginPickImageDeviceCancelClickMultipleTimes { + if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { return; } FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickVideo" + [FlutterMethodCall methodCallWithMethodName:@"pickImage" arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; [plugin handleMethodCall:call result:^(id _Nullable r){ }]; - XCTAssertEqual([plugin getImagePickerController].cameraDevice, - UIImagePickerControllerCameraDeviceFront); + plugin.result = ^(id result) { + + }; + // To ensure the flow does not crash by multiple cancel call + [plugin imagePickerControllerDidCancel:[plugin getImagePickerController]]; + [plugin imagePickerControllerDidCancel:[plugin getImagePickerController]]; } #pragma mark - Test video duration + - (void)testPickingVideoWithDuration { FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; FlutterMethodCall *call = [FlutterMethodCall diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m index 7c91606ba535..4084ae65b5e0 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m @@ -37,7 +37,6 @@ @interface FLTImagePickerPlugin () *)registrar { @@ -70,6 +69,21 @@ - (UIViewController *)viewControllerWithWindow:(UIWindow *)window { return topController; } +/** + * Returns the UIImagePickerControllerCameraDevice to use given [arguments]. + * + * If the cameraDevice value that is fetched from arguments is 1 then returns + * UIImagePickerControllerCameraDeviceFront. If the cameraDevice value that is fetched + * from arguments is 0 then returns UIImagePickerControllerCameraDeviceRear. + * + * @param arguments that should be used to get cameraDevice value. + */ +- (UIImagePickerControllerCameraDevice)getCameraDeviceFromArguments:(NSDictionary *)arguments { + NSInteger cameraDevice = [[arguments objectForKey:@"cameraDevice"] intValue]; + return (cameraDevice == 1) ? UIImagePickerControllerCameraDeviceFront + : UIImagePickerControllerCameraDeviceRear; +} + - (void)pickImageWithPHPicker:(int)maxImagesAllowed API_AVAILABLE(ios(14)) { PHPickerConfiguration *config = [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary]; @@ -95,13 +109,9 @@ - (void)pickImageWithUIImagePicker { self.maxImagesAllowed = 1; switch (imageSource) { - case SOURCE_CAMERA: { - NSInteger cameraDevice = [[_arguments objectForKey:@"cameraDevice"] intValue]; - _device = (cameraDevice == 1) ? UIImagePickerControllerCameraDeviceFront - : UIImagePickerControllerCameraDeviceRear; + case SOURCE_CAMERA: [self checkCameraAuthorization]; break; - } case SOURCE_GALLERY: [self checkPhotoAuthorization]; break; @@ -188,11 +198,12 @@ - (void)showCamera { return; } } + UIImagePickerControllerCameraDevice device = [self getCameraDeviceFromArguments:_arguments]; // Camera is not available on simulators if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] && - [UIImagePickerController isCameraDeviceAvailable:_device]) { + [UIImagePickerController isCameraDeviceAvailable:device]) { _imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera; - _imagePickerController.cameraDevice = _device; + _imagePickerController.cameraDevice = device; [[self viewControllerWithWindow:nil] presentViewController:_imagePickerController animated:YES completion:nil]; @@ -406,8 +417,8 @@ - (void)picker:(PHPickerViewController *)picker * The difference with initWithCapacity is that initWithCapacity still gives an empty array making * it impossible to add objects on an index larger than the size. * - * @param @size The length of the required array - * @return @NSMutableArray An array of a specified size + * @param size The length of the required array + * @return NSMutableArray An array of a specified size */ - (NSMutableArray *)createNSMutableArrayWithSize:(NSUInteger)size { NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:size]; @@ -528,14 +539,14 @@ - (void)saveImageWithPickerInfo:(NSDictionary *)info * Applies NSMutableArray on the FLutterResult. * * NSString must be returned by FlutterResult if the single image - * mode is active. It is checked by @c maxImagesAllowed and - * returns the first object of the @c pathlist. + * mode is active. It is checked by maxImagesAllowed and + * returns the first object of the pathlist. * * NSMutableArray must be returned by FlutterResult if the multi-image - * mode is active. After the @c pathlist count is checked then it returns - * the @c pathlist. + * mode is active. After the pathlist count is checked then it returns + * the pathlist. * - * @param @pathList that should be applied to FlutterResult. + * @param pathList that should be applied to FlutterResult. */ - (void)handleSavedPathList:(NSArray *)pathList { if (!self.result) { diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index bcda757b4bbf..c9866dbcda02 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.1+3 +version: 0.8.1+4 environment: sdk: ">=2.12.0 <3.0.0" From dd2d7397379e13a79b25f8327246e8f9bd0d21e5 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Fri, 16 Jul 2021 03:14:50 +0200 Subject: [PATCH 005/123] [image_picker] Migrate image_picker to package:cross_file (#4073) --- .../image_picker/image_picker/CHANGELOG.md | 9 + packages/image_picker/image_picker/README.md | 84 +--- .../image_picker/example/lib/main.dart | 14 +- .../image_picker/lib/image_picker.dart | 148 +++++- .../image_picker/image_picker/pubspec.yaml | 6 +- .../test/image_picker_deprecated_test.dart | 458 ++++++++++++++++++ .../image_picker/test/image_picker_test.dart | 82 ++-- 7 files changed, 686 insertions(+), 115 deletions(-) create mode 100644 packages/image_picker/image_picker/test/image_picker_deprecated_test.dart diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 33178dee8999..fef3e47cdf1a 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.8.2 + +* Added new methods that return `package:cross_file` `XFile` instances. [Docs](https://pub.dev/documentation/cross_file/latest/index.html). +* Deprecate methods that return `PickedFile` instances: + * `getImage`: use **`pickImage`** instead. + * `getVideo`: use **`pickVideo`** instead. + * `getMultiImage`: use **`pickMultiImage`** instead. + * `getLostData`: use **`retrieveLostData`** instead. + ## 0.8.1+4 * Fixes an issue where `preferredCameraDevice` option is not working for `getVideo` method. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index 3b3746d9f63e..18fd96d890fd 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -12,7 +12,7 @@ First, add `image_picker` as a [dependency in your pubspec.yaml file](https://fl ### iOS Starting with version **0.8.1** the iOS implementation uses PHPicker to pick (multiple) images on iOS 14 or higher. -As a result of implementing PHPicker it becomes impossible to pick HEIC images on the iOS simulator in iOS 14+. This is a known issue. Please test this on a real device, or test with non-HEIC images until Apple solves this issue.[63426347 - Apple known issue](https://www.google.com/search?q=63426347+apple&sxsrf=ALeKk01YnTMid5S0PYvhL8GbgXJ40ZS[…]t=gws-wiz&ved=0ahUKEwjKh8XH_5HwAhWL_rsIHUmHDN8Q4dUDCA8&uact=5) +As a result of implementing PHPicker it becomes impossible to pick HEIC images on the iOS simulator in iOS 14+. This is a known issue. Please test this on a real device, or test with non-HEIC images until Apple solves this issue.[63426347 - Apple known issue](https://www.google.com/search?q=63426347+apple&sxsrf=ALeKk01YnTMid5S0PYvhL8GbgXJ40ZS[…]t=gws-wiz&ved=0ahUKEwjKh8XH_5HwAhWL_rsIHUmHDN8Q4dUDCA8&uact=5) Add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: @@ -37,7 +37,19 @@ If you require your picked image to be stored permanently, it is your responsibi import 'package:image_picker/image_picker.dart'; ... - final PickedFile? pickedFile = await picker.getImage(source: ImageSource.camera); + final ImagePicker _picker = ImagePicker(); + // Pick an image + final XFile? image = await _picker.pickImage(source: ImageSource.gallery); + // Capture a photo + final XFile? photo = await _picker.pickImage(source: ImageSource.camera); + // Pick a video + final XFile? image = await _picker.pickVideo(source: ImageSource.gallery); + // Capture a video + final XFile? photo = await _picker.pickVideo(source: ImageSource.camera); + // Pick multiple images + final List? images = await _picker.pickMultiImage(source: ImageSource.gallery); + // Pick multiple photos + final List? photos = await _picker.pickMultiImage(source: ImageSource.camera); ... ``` @@ -46,9 +58,9 @@ import 'package:image_picker/image_picker.dart'; Android system -- although very rarely -- sometimes kills the MainActivity after the image_picker finishes. When this happens, we lost the data selected from the image_picker. You can use `retrieveLostData` to retrieve the lost data in this situation. For example: ```dart -Future retrieveLostData() async { - final LostData response = - await picker.getLostData(); +Future getLostData() async { + final LostDataResponse response = + await picker.retrieveLostData(); if (response.isEmpty) { return; } @@ -68,65 +80,17 @@ Future retrieveLostData() async { There's no way to detect when this happens, so calling this method at the right place is essential. We recommend to wire this into some kind of start up check. Please refer to the example app to see how we used it. -On Android, `getLostData` will only get the last picked image when picking multiple images, see: [#84634](https://github.com/flutter/flutter/issues/84634). +On Android, `retrieveLostData` will only get the last picked image when picking multiple images, see: [#84634](https://github.com/flutter/flutter/issues/84634). -## Deprecation warnings in `pickImage`, `pickVideo` and `LostDataResponse` +## Migrating to 0.8.2+ -Starting with version **0.6.7** of the image_picker plugin, the API of the plugin changed slightly to allow for web implementations to exist. - -The **old methods that returned `dart:io` File objects were marked as deprecated**, and a new set of methods that return [`PickedFile` objects](https://pub.dev/documentation/image_picker_platform_interface/latest/image_picker_platform_interface/PickedFile-class.html) were introduced. - -### How to migrate from to ^0.6.7 - -#### Instantiate the `ImagePicker` - -The new ImagePicker API does not rely in static methods anymore, so the first thing you'll need to do is to create a new instance of the plugin where you need it: - -```dart -final _picker = ImagePicker(); -``` +Starting with version **0.8.2** of the image_picker plugin, new methods have been added for picking files that return `XFile` instances (from the [cross_file](https://pub.dev/packages/cross_file) package) rather than the plugin's own `PickedFile` instances. While the previous methods still exist, it is already recommended to start migrating over to their new equivalents. Eventually, `PickedFile` and the methods that return instances of it will be deprecated and removed. #### Call the new methods -The new methods **receive the same parameters as before**, but they **return a `PickedFile`, instead of a `File`**. The `LostDataResponse` class has been replaced by the [`LostData` class](https://pub.dev/documentation/image_picker_platform_interface/latest/image_picker_platform_interface/LostData-class.html). - | Old API | New API | |---------|---------| -| `File image = await ImagePicker.pickImage(...)` | `PickedFile image = await _picker.getImage(...)` | -| `File video = await ImagePicker.pickVideo(...)` | `PickedFile video = await _picker.getVideo(...)` | -| `LostDataResponse response = await ImagePicker.retrieveLostData()` | `LostData response = await _picker.getLostData()` | - -#### `PickedFile` to `File` - -If your app needs dart:io `File` objects to operate, you may transform `PickedFile` to `File` like so: - -```dart -final pickedFile = await _picker.getImage(...); -final File file = File(pickedFile.path); -``` - -You may also retrieve the bytes from the pickedFile directly if needed: - -```dart -final bytes = await pickedFile.readAsBytes(); -``` - -#### Getting ready for the web platform - -Note that on the web platform (`kIsWeb == true`), `File` is not available, so the `path` of the `PickedFile` will point to a network resource instead: - -```dart -if (kIsWeb) { - image = Image.network(pickedFile.path); -} else { - image = Image.file(File(pickedFile.path)); -} -``` - -Alternatively, the code may be unified at the expense of memory utilization: - -```dart -image = Image.memory(await pickedFile.readAsBytes()) -``` - -Take a look at the changes to the `example` app introduced in version 0.6.7 to see the migration steps applied there. +| `PickedFile image = await _picker.getImage(...)` | `XFile image = await _picker.pickImage(...)` | +| `List images = await _picker.getMultiImage(...)` | `List images = await _picker.pickMultiImage(...)` | +| `PickedFile video = await _picker.getVideo(...)` | `XFile video = await _picker.pickVideo(...)` | +| `LostData response = await _picker.getLostData()` | `LostDataResponse response = await _picker.retrieveLostData()` | \ No newline at end of file diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 71388ef5db2f..2d5fd9aee4a7 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -36,9 +36,9 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _imageFileList; - set _imageFile(PickedFile? value) { + set _imageFile(XFile? value) { _imageFileList = value == null ? null : [value]; } @@ -54,7 +54,7 @@ class _MyHomePageState extends State { final TextEditingController maxHeightController = TextEditingController(); final TextEditingController qualityController = TextEditingController(); - Future _playVideo(PickedFile? file) async { + Future _playVideo(XFile? file) async { if (file != null && mounted) { await _disposeVideoController(); late VideoPlayerController controller; @@ -84,14 +84,14 @@ class _MyHomePageState extends State { await _controller!.setVolume(0.0); } if (isVideo) { - final PickedFile? file = await _picker.getVideo( + final XFile? file = await _picker.pickVideo( source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); } else if (isMultiImage) { await _displayPickImageDialog(context!, (double? maxWidth, double? maxHeight, int? quality) async { try { - final pickedFileList = await _picker.getMultiImage( + final pickedFileList = await _picker.pickMultiImage( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: quality, @@ -109,7 +109,7 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context!, (double? maxWidth, double? maxHeight, int? quality) async { try { - final pickedFile = await _picker.getImage( + final pickedFile = await _picker.pickImage( source: source, maxWidth: maxWidth, maxHeight: maxHeight, @@ -214,7 +214,7 @@ class _MyHomePageState extends State { } Future retrieveLostData() async { - final LostData response = await _picker.getLostData(); + final LostDataResponse response = await _picker.retrieveLostData(); if (response.isEmpty) { return; } diff --git a/packages/image_picker/image_picker/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart index 3d08a38d9f6e..5bc99d7f0bb2 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -2,12 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package - import 'dart:async'; - import 'package:flutter/foundation.dart'; - import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; export 'package:image_picker_platform_interface/image_picker_platform_interface.dart' @@ -17,7 +13,9 @@ export 'package:image_picker_platform_interface/image_picker_platform_interface. ImageSource, CameraDevice, LostData, + LostDataResponse, PickedFile, + XFile, RetrieveType; /// Provides an easy way to pick an image/video from the image library, @@ -61,6 +59,7 @@ class ImagePicker { /// the camera or photos gallery, no camera is available, plugin is already in use, /// temporary file could not be created (iOS only), plugin activity could not /// be allocated (Android only) or due to an unknown error. + @Deprecated('Switch to using pickImage instead') Future getImage({ required ImageSource source, double? maxWidth, @@ -101,6 +100,7 @@ class ImagePicker { /// be allocated (Android only) or due to an unknown error. /// /// See also [getImage] to allow users to only pick a single image. + @Deprecated('Switch to using pickMultiImage instead') Future?> getMultiImage({ double? maxWidth, double? maxHeight, @@ -135,6 +135,7 @@ class ImagePicker { /// temporary file could not be created and video could not be cached (iOS only), /// plugin activity could not be allocated (Android only) or due to an unknown error. /// + @Deprecated('Switch to using pickVideo instead') Future getVideo({ required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, @@ -160,7 +161,146 @@ class ImagePicker { /// See also: /// * [LostData], for what's included in the response. /// * [Android Activity Lifecycle](https://developer.android.com/reference/android/app/Activity.html), for more information on MainActivity destruction. + @Deprecated('Switch to using retrieveLostData instead') Future getLostData() { return platform.retrieveLostData(); } + + /// Returns an [XFile] object wrapping the image that was picked. + /// + /// The returned [XFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// + /// The `source` argument controls where the image comes from. This can + /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and above only support HEIC images if used + /// in addition to a size modification, of which the usage is explained below. + /// + /// If specified, the image will be at most `maxWidth` wide and + /// `maxHeight` tall. Otherwise the image will be returned at it's + /// original width and height. + /// The `imageQuality` argument modifies the quality of the image, ranging from 0-100 + /// where 100 is the original/max quality. If `imageQuality` is null, the image with + /// the original quality will be returned. Compression is only supported for certain + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not supported for the image that is picked, + /// a warning message will be logged. + /// + /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. + /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. + /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter for an intent to specify if + /// the front or rear camera should be opened, this function is not guaranteed + /// to work on an Android device. + /// + /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost + /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. + /// + /// See also [pickMultiImage] to allow users to select multiple images at once. + /// + /// The method could throw [PlatformException] if the app does not have permission to access + /// the camera or photos gallery, no camera is available, plugin is already in use, + /// temporary file could not be created (iOS only), plugin activity could not + /// be allocated (Android only) or due to an unknown error. + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) { + return platform.getImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ); + } + + /// Returns a [List] object wrapping the images that were picked. + /// + /// The returned [List] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and above only support HEIC images if used + /// in addition to a size modification, of which the usage is explained below. + /// + /// This method is not supported in iOS versions lower than 14. + /// + /// If specified, the images will be at most `maxWidth` wide and + /// `maxHeight` tall. Otherwise the images will be returned at it's + /// original width and height. + /// The `imageQuality` argument modifies the quality of the images, ranging from 0-100 + /// where 100 is the original/max quality. If `imageQuality` is null, the images with + /// the original quality will be returned. Compression is only supported for certain + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not supported for the image that is picked, + /// a warning message will be logged. + /// + /// The method could throw [PlatformException] if the app does not have permission to access + /// the camera or photos gallery, no camera is available, plugin is already in use, + /// temporary file could not be created (iOS only), plugin activity could not + /// be allocated (Android only) or due to an unknown error. + /// + /// See also [pickImage] to allow users to only pick a single image. + Future?> pickMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) { + return platform.getMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ); + } + + /// Returns an [XFile] object wrapping the video that was picked. + /// + /// The returned [XFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// + /// The [source] argument controls where the video comes from. This can + /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// + /// The [maxDuration] argument specifies the maximum duration of the captured video. If no [maxDuration] is specified, + /// the maximum duration will be infinite. + /// + /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. + /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. + /// Defaults to [CameraDevice.rear]. + /// + /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost + /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. + /// + /// The method could throw [PlatformException] if the app does not have permission to access + /// the camera or photos gallery, no camera is available, plugin is already in use, + /// temporary file could not be created and video could not be cached (iOS only), + /// plugin activity could not be allocated (Android only) or due to an unknown error. + /// + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) { + return platform.getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration, + ); + } + + /// Retrieve the lost [XFile] when [pickImage], [pickMultiImage] or [pickVideo] failed because the MainActivity + /// is destroyed. (Android only) + /// + /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. + /// Call this method to retrieve the lost data and process the data according to your APP's business logic. + /// + /// Returns a [LostDataResponse] object if successfully retrieved the lost data. The [LostDataResponse] object can \ + /// represent either a successful image/video selection, or a failure. + /// + /// Calling this on a non-Android platform will throw [UnimplementedError] exception. + /// + /// See also: + /// * [LostDataResponse], for what's included in the response. + /// * [Android Activity Lifecycle](https://developer.android.com/reference/android/app/Activity.html), for more information on MainActivity destruction. + Future retrieveLostData() { + return platform.getLostData(); + } } diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index c9866dbcda02..e5ecfeb22232 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.1+4 +version: 0.8.2 environment: sdk: ">=2.12.0 <3.0.0" @@ -24,8 +24,8 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 - image_picker_for_web: ^2.0.0 - image_picker_platform_interface: ^2.1.0 + image_picker_for_web: ^2.1.0 + image_picker_platform_interface: ^2.2.0 dev_dependencies: flutter_test: diff --git a/packages/image_picker/image_picker/test/image_picker_deprecated_test.dart b/packages/image_picker/image_picker/test/image_picker_deprecated_test.dart new file mode 100644 index 000000000000..f295e3d02f66 --- /dev/null +++ b/packages/image_picker/image_picker/test/image_picker_deprecated_test.dart @@ -0,0 +1,458 @@ +// 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +// This file preserves the tests for the deprecated methods as they were before +// the migration. See image_picker_test.dart for the current tests. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$ImagePicker', () { + const MethodChannel channel = + MethodChannel('plugins.flutter.io/image_picker'); + + final List log = []; + + final picker = ImagePicker(); + + test('ImagePicker platform instance overrides the actual platform used', + () { + final ImagePickerPlatform savedPlatform = ImagePickerPlatform.instance; + final MockPlatform mockPlatform = MockPlatform(); + ImagePickerPlatform.instance = mockPlatform; + expect(ImagePicker.platform, mockPlatform); + ImagePickerPlatform.instance = savedPlatform; + }); + + group('#Single image/video', () { + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return ''; + }); + + log.clear(); + }); + + group('#pickImage', () { + test('passes the image source argument correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 1, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxHeight: 10.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.getImage( + source: ImageSource.camera, maxWidth: 10.0, imageQuality: 70); + await picker.getImage( + source: ImageSource.camera, maxHeight: 10.0, imageQuality: 70); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + picker.getImage(source: ImageSource.camera, maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + picker.getImage(source: ImageSource.camera, maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getImage(source: ImageSource.gallery), isNull); + expect(await picker.getImage(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getImage(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + group('#pickVideo', () { + test('passes the image source argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + isMethodCall('pickVideo', arguments: { + 'source': 1, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('passes the duration argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10)); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1)); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(hours: 1)); + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 10, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 60, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 3600, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('handles a null video path response gracefully', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getVideo(source: ImageSource.gallery), isNull); + expect(await picker.getVideo(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getVideo(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + group('#retrieveLostData', () { + test('retrieveLostData get success response', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path', + }; + }); + final LostData response = await picker.getLostData(); + expect(response.type, RetrieveType.image); + expect(response.file!.path, '/example/path'); + }); + + test('retrieveLostData get error response', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + }; + }); + final LostData response = await picker.getLostData(); + expect(response.type, RetrieveType.video); + expect(response.exception!.code, 'test_error_code'); + expect(response.exception!.message, 'test_error_message'); + }); + + test('retrieveLostData get null response', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return null; + }); + expect((await picker.getLostData()).isEmpty, true); + }); + + test('retrieveLostData get both path and error should throw', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + 'path': '/example/path', + }; + }); + expect(picker.getLostData(), throwsAssertionError); + }); + }); + }); + + group('Multi images', () { + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return []; + }); + log.clear(); + }); + + group('#pickMultiImage', () { + test('passes the width and height arguments correctly', () async { + await picker.getMultiImage(); + await picker.getMultiImage( + maxWidth: 10.0, + ); + await picker.getMultiImage( + maxHeight: 10.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + picker.getMultiImage(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + picker.getMultiImage(maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getMultiImage(), isNull); + expect(await picker.getMultiImage(), isNull); + }); + }); + }); + }); +} + +class MockPlatform extends Mock + with MockPlatformInterfaceMixin + implements ImagePickerPlatform {} diff --git a/packages/image_picker/image_picker/test/image_picker_test.dart b/packages/image_picker/image_picker/test/image_picker_test.dart index d83b403d1d45..960dfe6917ea 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.dart @@ -41,8 +41,8 @@ void main() { group('#pickImage', () { test('passes the image source argument correctly', () async { - await picker.getImage(source: ImageSource.camera); - await picker.getImage(source: ImageSource.gallery); + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage(source: ImageSource.gallery); expect( log, @@ -66,25 +66,25 @@ void main() { }); test('passes the width and height arguments correctly', () async { - await picker.getImage(source: ImageSource.camera); - await picker.getImage( + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage( source: ImageSource.camera, maxWidth: 10.0, ); - await picker.getImage( + await picker.pickImage( source: ImageSource.camera, maxHeight: 10.0, ); - await picker.getImage( + await picker.pickImage( source: ImageSource.camera, maxWidth: 10.0, maxHeight: 20.0, ); - await picker.getImage( + await picker.pickImage( source: ImageSource.camera, maxWidth: 10.0, imageQuality: 70); - await picker.getImage( + await picker.pickImage( source: ImageSource.camera, maxHeight: 10.0, imageQuality: 70); - await picker.getImage( + await picker.pickImage( source: ImageSource.camera, maxWidth: 10.0, maxHeight: 20.0, @@ -148,12 +148,12 @@ void main() { test('does not accept a negative width or height argument', () { expect( - picker.getImage(source: ImageSource.camera, maxWidth: -1.0), + picker.pickImage(source: ImageSource.camera, maxWidth: -1.0), throwsArgumentError, ); expect( - picker.getImage(source: ImageSource.camera, maxHeight: -1.0), + picker.pickImage(source: ImageSource.camera, maxHeight: -1.0), throwsArgumentError, ); }); @@ -161,12 +161,12 @@ void main() { test('handles a null image path response gracefully', () async { channel.setMockMethodCallHandler((MethodCall methodCall) => null); - expect(await picker.getImage(source: ImageSource.gallery), isNull); - expect(await picker.getImage(source: ImageSource.camera), isNull); + expect(await picker.pickImage(source: ImageSource.gallery), isNull); + expect(await picker.pickImage(source: ImageSource.camera), isNull); }); test('camera position defaults to back', () async { - await picker.getImage(source: ImageSource.camera); + await picker.pickImage(source: ImageSource.camera); expect( log, @@ -183,7 +183,7 @@ void main() { }); test('camera position can set to front', () async { - await picker.getImage( + await picker.pickImage( source: ImageSource.camera, preferredCameraDevice: CameraDevice.front); @@ -204,8 +204,8 @@ void main() { group('#pickVideo', () { test('passes the image source argument correctly', () async { - await picker.getVideo(source: ImageSource.camera); - await picker.getVideo(source: ImageSource.gallery); + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo(source: ImageSource.gallery); expect( log, @@ -225,14 +225,14 @@ void main() { }); test('passes the duration argument correctly', () async { - await picker.getVideo(source: ImageSource.camera); - await picker.getVideo( + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo( source: ImageSource.camera, maxDuration: const Duration(seconds: 10)); - await picker.getVideo( + await picker.pickVideo( source: ImageSource.camera, maxDuration: const Duration(minutes: 1)); - await picker.getVideo( + await picker.pickVideo( source: ImageSource.camera, maxDuration: const Duration(hours: 1)); expect( @@ -265,12 +265,12 @@ void main() { test('handles a null video path response gracefully', () async { channel.setMockMethodCallHandler((MethodCall methodCall) => null); - expect(await picker.getVideo(source: ImageSource.gallery), isNull); - expect(await picker.getVideo(source: ImageSource.camera), isNull); + expect(await picker.pickVideo(source: ImageSource.gallery), isNull); + expect(await picker.pickVideo(source: ImageSource.camera), isNull); }); test('camera position defaults to back', () async { - await picker.getVideo(source: ImageSource.camera); + await picker.pickVideo(source: ImageSource.camera); expect( log, @@ -285,7 +285,7 @@ void main() { }); test('camera position can set to front', () async { - await picker.getVideo( + await picker.pickVideo( source: ImageSource.camera, preferredCameraDevice: CameraDevice.front); @@ -310,7 +310,7 @@ void main() { 'path': '/example/path', }; }); - final LostData response = await picker.getLostData(); + final LostDataResponse response = await picker.retrieveLostData(); expect(response.type, RetrieveType.image); expect(response.file!.path, '/example/path'); }); @@ -323,7 +323,7 @@ void main() { 'errorMessage': 'test_error_message', }; }); - final LostData response = await picker.getLostData(); + final LostDataResponse response = await picker.retrieveLostData(); expect(response.type, RetrieveType.video); expect(response.exception!.code, 'test_error_code'); expect(response.exception!.message, 'test_error_message'); @@ -333,7 +333,7 @@ void main() { channel.setMockMethodCallHandler((MethodCall methodCall) async { return null; }); - expect((await picker.getLostData()).isEmpty, true); + expect((await picker.retrieveLostData()).isEmpty, true); }); test('retrieveLostData get both path and error should throw', () async { @@ -345,12 +345,12 @@ void main() { 'path': '/example/path', }; }); - expect(picker.getLostData(), throwsAssertionError); + expect(picker.retrieveLostData(), throwsAssertionError); }); }); }); - group('Multi images', () { + group('#Multi images', () { setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); @@ -361,26 +361,26 @@ void main() { group('#pickMultiImage', () { test('passes the width and height arguments correctly', () async { - await picker.getMultiImage(); - await picker.getMultiImage( + await picker.pickMultiImage(); + await picker.pickMultiImage( maxWidth: 10.0, ); - await picker.getMultiImage( + await picker.pickMultiImage( maxHeight: 10.0, ); - await picker.getMultiImage( + await picker.pickMultiImage( maxWidth: 10.0, maxHeight: 20.0, ); - await picker.getMultiImage( + await picker.pickMultiImage( maxWidth: 10.0, imageQuality: 70, ); - await picker.getMultiImage( + await picker.pickMultiImage( maxHeight: 10.0, imageQuality: 70, ); - await picker.getMultiImage( + await picker.pickMultiImage( maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); expect( @@ -427,12 +427,12 @@ void main() { test('does not accept a negative width or height argument', () { expect( - picker.getMultiImage(maxWidth: -1.0), + picker.pickMultiImage(maxWidth: -1.0), throwsArgumentError, ); expect( - picker.getMultiImage(maxHeight: -1.0), + picker.pickMultiImage(maxHeight: -1.0), throwsArgumentError, ); }); @@ -440,8 +440,8 @@ void main() { test('handles a null image path response gracefully', () async { channel.setMockMethodCallHandler((MethodCall methodCall) => null); - expect(await picker.getMultiImage(), isNull); - expect(await picker.getMultiImage(), isNull); + expect(await picker.pickMultiImage(), isNull); + expect(await picker.pickMultiImage(), isNull); }); }); }); From f726c319a61dd50662cdf5a52e9e4ab12b6ec2c8 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Fri, 16 Jul 2021 11:16:03 +0200 Subject: [PATCH 006/123] [camera] Fix coordinate rotation for setting focus- and exposure points on iOS (#4158) --- packages/camera/camera/CHANGELOG.md | 4 ++ .../ios/RunnerTests/CameraExposureTests.m | 55 +++++++++++++++++++ .../ios/RunnerTests/CameraFocusTests.m | 27 ++++++++- .../example/ios/RunnerTests/CameraUtilTests.m | 49 +++++++++++++++++ .../camera/camera/ios/Classes/CameraPlugin.m | 37 ++++++++++++- packages/camera/camera/pubspec.yaml | 2 +- 6 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 packages/camera/camera/example/ios/RunnerTests/CameraExposureTests.m create mode 100644 packages/camera/camera/example/ios/RunnerTests/CameraUtilTests.m diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 1f30104218e3..236cf96f027a 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.1+5 + +* Make sure the `setFocusPoint` and `setExposurePoint` coordinates work correctly in all orientations on iOS (instead of only in portrait mode). + ## 0.8.1+4 * Silenced warnings that may occur during build when using a very diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraExposureTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraExposureTests.m new file mode 100644 index 000000000000..ee43d3f155f4 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/CameraExposureTests.m @@ -0,0 +1,55 @@ +// 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 camera; +@import XCTest; +@import AVFoundation; +#import + +@interface FLTCam : NSObject + +- (void)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y; +@end + +@interface CameraExposureTests : XCTestCase +@property(readonly, nonatomic) FLTCam *camera; +@property(readonly, nonatomic) id mockDevice; +@property(readonly, nonatomic) id mockUIDevice; +@end + +@implementation CameraExposureTests + +- (void)setUp { + _camera = [[FLTCam alloc] init]; + _mockDevice = OCMClassMock([AVCaptureDevice class]); + _mockUIDevice = OCMPartialMock([UIDevice currentDevice]); +} + +- (void)tearDown { + [_mockDevice stopMocking]; + [_mockUIDevice stopMocking]; +} + +- (void)testSetExpsourePointWithResult_SetsExposurePointOfInterest { + // UI is currently in landscape left orientation + OCMStub([(UIDevice *)_mockUIDevice orientation]).andReturn(UIDeviceOrientationLandscapeLeft); + // Exposure point of interest is supported + OCMStub([_mockDevice isExposurePointOfInterestSupported]).andReturn(true); + // Set mock device as the current capture device + [_camera setValue:_mockDevice forKey:@"captureDevice"]; + + // Run test + [_camera + setExposurePointWithResult:^void(id _Nullable result) { + } + x:1 + y:1]; + + // Verify the focus point of interest has been set + OCMVerify([_mockDevice setExposurePointOfInterest:CGPointMake(1, 1)]); +} + +@end diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m index 5d93bdf70332..27537e7ebdac 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m @@ -19,12 +19,13 @@ @interface FLTCam : NSObject + +@interface FLTCam : NSObject + +- (CGPoint)getCGPointForCoordsWithOrientation:(UIDeviceOrientation)orientation + x:(double)x + y:(double)y; + +@end + +@interface CameraUtilTests : XCTestCase +@property(readonly, nonatomic) FLTCam *camera; + +@end + +@implementation CameraUtilTests + +- (void)setUp { + _camera = [[FLTCam alloc] init]; +} + +- (void)testGetCGPointForCoordsWithOrientation_ShouldRotateCoords { + CGPoint point; + point = [_camera getCGPointForCoordsWithOrientation:UIDeviceOrientationLandscapeLeft x:1 y:1]; + XCTAssertTrue(CGPointEqualToPoint(point, CGPointMake(1, 1)), + @"Resulting coordinates are invalid."); + point = [_camera getCGPointForCoordsWithOrientation:UIDeviceOrientationPortrait x:0 y:1]; + XCTAssertTrue(CGPointEqualToPoint(point, CGPointMake(1, 1)), + @"Resulting coordinates are invalid."); + point = [_camera getCGPointForCoordsWithOrientation:UIDeviceOrientationLandscapeRight x:0 y:0]; + XCTAssertTrue(CGPointEqualToPoint(point, CGPointMake(1, 1)), + @"Resulting coordinates are invalid."); + point = [_camera getCGPointForCoordsWithOrientation:UIDeviceOrientationPortraitUpsideDown + x:1 + y:0]; + XCTAssertTrue(CGPointEqualToPoint(point, CGPointMake(1, 1)), + @"Resulting coordinates are invalid."); +} + +@end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index ebd5366ba78d..d88eb45945fe 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -1030,6 +1030,31 @@ - (void)applyFocusMode:(FocusMode)focusMode onDevice:(AVCaptureDevice *)captureD [captureDevice unlockForConfiguration]; } +- (CGPoint)getCGPointForCoordsWithOrientation:(UIDeviceOrientation)orientation + x:(double)x + y:(double)y { + double oldX = x, oldY = y; + switch (orientation) { + case UIDeviceOrientationPortrait: // 90 ccw + y = 1 - oldX; + x = oldY; + break; + case UIDeviceOrientationPortraitUpsideDown: // 90 cw + x = 1 - oldY; + y = oldX; + break; + case UIDeviceOrientationLandscapeRight: // 180 + x = 1 - x; + y = 1 - y; + break; + case UIDeviceOrientationLandscapeLeft: + default: + // No rotation required + break; + } + return CGPointMake(x, y); +} + - (void)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y { if (!_captureDevice.isExposurePointOfInterestSupported) { result([FlutterError errorWithCode:@"setExposurePointFailed" @@ -1037,8 +1062,11 @@ - (void)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y details:nil]); return; } + UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; [_captureDevice lockForConfiguration:nil]; - [_captureDevice setExposurePointOfInterest:CGPointMake(y, 1 - x)]; + [_captureDevice setExposurePointOfInterest:[self getCGPointForCoordsWithOrientation:orientation + x:x + y:y]]; [_captureDevice unlockForConfiguration]; // Retrigger auto exposure [self applyExposureMode]; @@ -1052,11 +1080,16 @@ - (void)setFocusPointWithResult:(FlutterResult)result x:(double)x y:(double)y { details:nil]); return; } + UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; [_captureDevice lockForConfiguration:nil]; - [_captureDevice setFocusPointOfInterest:CGPointMake(y, 1 - x)]; + + [_captureDevice setFocusPointOfInterest:[self getCGPointForCoordsWithOrientation:orientation + x:x + y:y]]; [_captureDevice unlockForConfiguration]; // Retrigger auto focus [self applyFocusMode]; + result(nil); } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 789910e2c79b..78eb49a999a2 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.8.1+4 +version: 0.8.1+5 environment: sdk: ">=2.12.0 <3.0.0" From 57fb51f06e18fcecbdf7d15cb60ff59a7671d150 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Sat, 17 Jul 2021 01:03:05 +0200 Subject: [PATCH 007/123] [camera] Introduce `camera_web` package (#4151) This first version is a no-op implementation of the platform_interface, but is the initial step to bring over the implementation from the Photobooth into flutter/plugins. Won't be published (for now). Co-authored-by: Felix Angelov --- packages/camera/camera_web/CHANGELOG.md | 3 + packages/camera/camera_web/LICENSE | 25 ++ packages/camera/camera_web/README.md | 7 + packages/camera/camera_web/example/README.md | 9 + .../integration_test/camera_web_test.dart | 314 ++++++++++++++++++ .../integration_test/helpers/helpers.dart | 5 + .../integration_test/helpers/mocks.dart | 23 ++ .../camera/camera_web/example/lib/main.dart | 18 + .../camera/camera_web/example/pubspec.yaml | 21 ++ .../camera/camera_web/example/run_test.sh | 22 ++ .../example/test_driver/integration_test.dart | 7 + .../camera/camera_web/example/web/index.html | 12 + .../camera/camera_web/lib/camera_web.dart | 7 + .../camera/camera_web/lib/src/camera_web.dart | 193 +++++++++++ packages/camera/camera_web/pubspec.yaml | 33 ++ packages/camera/camera_web/test/README.md | 5 + .../test/tests_exist_elsewhere_test.dart | 14 + 17 files changed, 718 insertions(+) create mode 100644 packages/camera/camera_web/CHANGELOG.md create mode 100644 packages/camera/camera_web/LICENSE create mode 100644 packages/camera/camera_web/README.md create mode 100644 packages/camera/camera_web/example/README.md create mode 100644 packages/camera/camera_web/example/integration_test/camera_web_test.dart create mode 100644 packages/camera/camera_web/example/integration_test/helpers/helpers.dart create mode 100644 packages/camera/camera_web/example/integration_test/helpers/mocks.dart create mode 100644 packages/camera/camera_web/example/lib/main.dart create mode 100644 packages/camera/camera_web/example/pubspec.yaml create mode 100755 packages/camera/camera_web/example/run_test.sh create mode 100644 packages/camera/camera_web/example/test_driver/integration_test.dart create mode 100644 packages/camera/camera_web/example/web/index.html create mode 100644 packages/camera/camera_web/lib/camera_web.dart create mode 100644 packages/camera/camera_web/lib/src/camera_web.dart create mode 100644 packages/camera/camera_web/pubspec.yaml create mode 100644 packages/camera/camera_web/test/README.md create mode 100644 packages/camera/camera_web/test/tests_exist_elsewhere_test.dart diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md new file mode 100644 index 000000000000..1318780830f8 --- /dev/null +++ b/packages/camera/camera_web/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* Initial release diff --git a/packages/camera/camera_web/LICENSE b/packages/camera/camera_web/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/camera/camera_web/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/camera/camera_web/README.md b/packages/camera/camera_web/README.md new file mode 100644 index 000000000000..d57bd7446d17 --- /dev/null +++ b/packages/camera/camera_web/README.md @@ -0,0 +1,7 @@ +# Camera Web Plugin + +A Flutter plugin for Web allowing access to the device cameras. + +*Note*: This plugin is under development. + +In order to use this plugin, your app should depend both on `camera` and `camera_web`. This is a temporary solution until a plugin is released. \ No newline at end of file diff --git a/packages/camera/camera_web/example/README.md b/packages/camera/camera_web/example/README.md new file mode 100644 index 000000000000..8a6e74b107ea --- /dev/null +++ b/packages/camera/camera_web/example/README.md @@ -0,0 +1,9 @@ +# Testing + +This package uses `package:integration_test` to run its tests in a web browser. + +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. + +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. \ No newline at end of file diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart new file mode 100644 index 000000000000..d26f0e855889 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -0,0 +1,314 @@ +// 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 'dart:html'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/camera_web.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraPlugin', () { + const cameraId = 0; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + late VideoElement videoElement; + + setUp(() async { + window = MockWindow(); + navigator = MockNavigator(); + mediaDevices = MockMediaDevices(); + videoElement = VideoElement() + ..src = + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4' + ..preload = 'true' + ..width = 10 + ..height = 10; + + when(() => window.navigator).thenReturn(navigator); + when(() => navigator.mediaDevices).thenReturn(mediaDevices); + when( + () => mediaDevices.getUserMedia(any()), + ).thenAnswer((_) async => videoElement.captureStream()); + + CameraPlatform.instance = CameraPlugin()..window = window; + }); + + testWidgets('CameraPlugin is the live instance', (tester) async { + expect(CameraPlatform.instance, isA()); + }); + + testWidgets('availableCameras throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.availableCameras(), + throwsUnimplementedError, + ); + }); + + testWidgets('createCamera throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.createCamera( + CameraDescription( + name: 'name', + lensDirection: CameraLensDirection.external, + sensorOrientation: 0, + ), + ResolutionPreset.medium, + ), + throwsUnimplementedError, + ); + }); + + testWidgets('initializeCamera throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('lockCaptureOrientation throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.landscapeLeft, + ), + throwsUnimplementedError, + ); + }); + + testWidgets('unlockCaptureOrientation throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.unlockCaptureOrientation(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('takePicture throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.takePicture(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('prepareForVideoRecording throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.prepareForVideoRecording(), + throwsUnimplementedError, + ); + }); + + testWidgets('startVideoRecording throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.startVideoRecording(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('stopVideoRecording throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.stopVideoRecording(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('pauseVideoRecording throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('resumeVideoRecording throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('setFlashMode throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.setFlashMode( + cameraId, + FlashMode.auto, + ), + throwsUnimplementedError, + ); + }); + + testWidgets('setExposureMode throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.setExposureMode( + cameraId, + ExposureMode.auto, + ), + throwsUnimplementedError, + ); + }); + + testWidgets('setExposurePoint throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.setExposurePoint( + cameraId, + const Point(0, 0), + ), + throwsUnimplementedError, + ); + }); + + testWidgets('getMinExposureOffset throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.getMinExposureOffset(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('getMaxExposureOffset throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.getMaxExposureOffset(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('getExposureOffsetStepSize throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.getExposureOffsetStepSize(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('setExposureOffset throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.setExposureOffset( + cameraId, + 0, + ), + throwsUnimplementedError, + ); + }); + + testWidgets('setFocusMode throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.setFocusMode( + cameraId, + FocusMode.auto, + ), + throwsUnimplementedError, + ); + }); + + testWidgets('setFocusPoint throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.setFocusPoint( + cameraId, + const Point(0, 0), + ), + throwsUnimplementedError, + ); + }); + + testWidgets('getMaxZoomLevel throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.getMaxZoomLevel(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('getMinZoomLevel throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.getMinZoomLevel(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('setZoomLevel throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.setZoomLevel( + cameraId, + 1.0, + ), + throwsUnimplementedError, + ); + }); + + testWidgets('buildPreview throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.buildPreview(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('dispose throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.dispose(cameraId), + throwsUnimplementedError, + ); + }); + + group('events', () { + testWidgets('onCameraInitialized throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.onCameraInitialized(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('onCameraResolutionChanged throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.onCameraResolutionChanged(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('onCameraClosing throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.onCameraClosing(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('onCameraError throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.onCameraError(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('onVideoRecordedEvent throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.onVideoRecordedEvent(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('onDeviceOrientationChanged throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.onDeviceOrientationChanged(), + throwsUnimplementedError, + ); + }); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/helpers/helpers.dart b/packages/camera/camera_web/example/integration_test/helpers/helpers.dart new file mode 100644 index 000000000000..7094f55bb62e --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/helpers/helpers.dart @@ -0,0 +1,5 @@ +// 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. + +export 'mocks.dart'; diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart new file mode 100644 index 000000000000..03be3f0b3ca6 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -0,0 +1,23 @@ +// 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 'dart:html'; + +import 'package:mocktail/mocktail.dart'; + +class MockWindow extends Mock implements Window {} + +class MockNavigator extends Mock implements Navigator {} + +class MockMediaDevices extends Mock implements MediaDevices {} + +/// A fake [DomException] that returns the provided [errorName]. +class FakeDomException extends Fake implements DomException { + FakeDomException(this.errorName); + + final String errorName; + + @override + String get name => errorName; +} diff --git a/packages/camera/camera_web/example/lib/main.dart b/packages/camera/camera_web/example/lib/main.dart new file mode 100644 index 000000000000..6e8f85e74f40 --- /dev/null +++ b/packages/camera/camera_web/example/lib/main.dart @@ -0,0 +1,18 @@ +// 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:flutter/material.dart'; + +void main() => runApp(MyApp()); + +/// App for testing +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: Text('Testing... Look at the console output for results!'), + ); + } +} diff --git a/packages/camera/camera_web/example/pubspec.yaml b/packages/camera/camera_web/example/pubspec.yaml new file mode 100644 index 000000000000..1e075712325e --- /dev/null +++ b/packages/camera/camera_web/example/pubspec.yaml @@ -0,0 +1,21 @@ +name: camera_web_integration_tests +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + mocktail: ^0.1.4 + camera_web: + path: ../ + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter diff --git a/packages/camera/camera_web/example/run_test.sh b/packages/camera/camera_web/example/run_test.sh new file mode 100755 index 000000000000..00482faa53df --- /dev/null +++ b/packages/camera/camera_web/example/run_test.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# 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. + +if pgrep -lf chromedriver > /dev/null; then + echo "chromedriver is running." + + if [ $# -eq 0 ]; then + echo "No target specified, running all tests..." + find integration_test/ -iname *_test.dart | xargs -n1 -I{} -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target='{}' + else + echo "Running test target: $1..." + set -x + flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target=$1 + fi + + else + echo "chromedriver is not running." + echo "Please, check the README.md for instructions on how to use run_test.sh" +fi + diff --git a/packages/camera/camera_web/example/test_driver/integration_test.dart b/packages/camera/camera_web/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/camera/camera_web/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// 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:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/camera/camera_web/example/web/index.html b/packages/camera/camera_web/example/web/index.html new file mode 100644 index 000000000000..f3c6a5e8a8e3 --- /dev/null +++ b/packages/camera/camera_web/example/web/index.html @@ -0,0 +1,12 @@ + + + + + Browser Tests + + + + + diff --git a/packages/camera/camera_web/lib/camera_web.dart b/packages/camera/camera_web/lib/camera_web.dart new file mode 100644 index 000000000000..dcefc9293b88 --- /dev/null +++ b/packages/camera/camera_web/lib/camera_web.dart @@ -0,0 +1,7 @@ +// 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. + +library camera_web; + +export 'src/camera_web.dart'; diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart new file mode 100644 index 000000000000..fc3be09eec1d --- /dev/null +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -0,0 +1,193 @@ +// 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 'dart:async'; +import 'dart:html' as html; +import 'dart:math'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +/// The web implementation of [CameraPlatform]. +/// +/// This class implements the `package:camera` functionality for the web. +class CameraPlugin extends CameraPlatform { + /// Registers this class as the default instance of [CameraPlatform]. + static void registerWith(Registrar registrar) { + CameraPlatform.instance = CameraPlugin(); + } + + /// The current browser window used to access device cameras. + @visibleForTesting + html.Window? window; + + @override + Future> availableCameras() { + throw UnimplementedError('availableCameras() is not implemented.'); + } + + @override + Future createCamera( + CameraDescription cameraDescription, + ResolutionPreset? resolutionPreset, { + bool enableAudio = false, + }) { + throw UnimplementedError('createCamera() is not implemented.'); + } + + @override + Future initializeCamera( + int cameraId, { + ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, + }) { + throw UnimplementedError('initializeCamera() is not implemented.'); + } + + @override + Stream onCameraInitialized(int cameraId) { + throw UnimplementedError('onCameraInitialized() is not implemented.'); + } + + @override + Stream onCameraResolutionChanged(int cameraId) { + throw UnimplementedError('onCameraResolutionChanged() is not implemented.'); + } + + @override + Stream onCameraClosing(int cameraId) { + throw UnimplementedError('onCameraClosing() is not implemented.'); + } + + @override + Stream onCameraError(int cameraId) { + throw UnimplementedError('onCameraError() is not implemented.'); + } + + @override + Stream onVideoRecordedEvent(int cameraId) { + throw UnimplementedError('onVideoRecordedEvent() is not implemented.'); + } + + @override + Stream onDeviceOrientationChanged() { + throw UnimplementedError( + 'onDeviceOrientationChanged() is not implemented.', + ); + } + + @override + Future lockCaptureOrientation( + int cameraId, + DeviceOrientation orientation, + ) { + throw UnimplementedError('lockCaptureOrientation() is not implemented.'); + } + + @override + Future unlockCaptureOrientation(int cameraId) { + throw UnimplementedError('unlockCaptureOrientation() is not implemented.'); + } + + @override + Future takePicture(int cameraId) { + throw UnimplementedError('takePicture() is not implemented.'); + } + + @override + Future prepareForVideoRecording() { + throw UnimplementedError('prepareForVideoRecording() is not implemented.'); + } + + @override + Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) { + throw UnimplementedError('startVideoRecording() is not implemented.'); + } + + @override + Future stopVideoRecording(int cameraId) { + throw UnimplementedError('stopVideoRecording() is not implemented.'); + } + + @override + Future pauseVideoRecording(int cameraId) { + throw UnimplementedError('pauseVideoRecording() is not implemented.'); + } + + @override + Future resumeVideoRecording(int cameraId) { + throw UnimplementedError('resumeVideoRecording() is not implemented.'); + } + + @override + Future setFlashMode(int cameraId, FlashMode mode) { + throw UnimplementedError('setFlashMode() is not implemented.'); + } + + @override + Future setExposureMode(int cameraId, ExposureMode mode) { + throw UnimplementedError('setExposureMode() is not implemented.'); + } + + @override + Future setExposurePoint(int cameraId, Point? point) { + throw UnimplementedError('setExposurePoint() is not implemented.'); + } + + @override + Future getMinExposureOffset(int cameraId) { + throw UnimplementedError('getMinExposureOffset() is not implemented.'); + } + + @override + Future getMaxExposureOffset(int cameraId) { + throw UnimplementedError('getMaxExposureOffset() is not implemented.'); + } + + @override + Future getExposureOffsetStepSize(int cameraId) { + throw UnimplementedError('getExposureOffsetStepSize() is not implemented.'); + } + + @override + Future setExposureOffset(int cameraId, double offset) { + throw UnimplementedError('setExposureOffset() is not implemented.'); + } + + @override + Future setFocusMode(int cameraId, FocusMode mode) { + throw UnimplementedError('setFocusMode() is not implemented.'); + } + + @override + Future setFocusPoint(int cameraId, Point? point) { + throw UnimplementedError('setFocusPoint() is not implemented.'); + } + + @override + Future getMaxZoomLevel(int cameraId) { + throw UnimplementedError('getMaxZoomLevel() is not implemented.'); + } + + @override + Future getMinZoomLevel(int cameraId) { + throw UnimplementedError('getMinZoomLevel() is not implemented.'); + } + + @override + Future setZoomLevel(int cameraId, double zoom) { + throw UnimplementedError('setZoomLevel() is not implemented.'); + } + + @override + Widget buildPreview(int cameraId) { + throw UnimplementedError('buildPreview() is not implemented.'); + } + + @override + Future dispose(int cameraId) { + throw UnimplementedError('dispose() is not implemented.'); + } +} diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml new file mode 100644 index 000000000000..a2aa43c22d65 --- /dev/null +++ b/packages/camera/camera_web/pubspec.yaml @@ -0,0 +1,33 @@ +name: camera_web +description: A Flutter plugin for getting information about and controlling the camera on Web. +repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera_web +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 +version: 0.1.0 + +# This plugin is under development and will be published +# when the first working web camera implementation is added. +# TODO(bselwe): Remove when camera_web should be published. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + platforms: + web: + pluginClass: CameraPlugin + fileName: camera_web.dart + +dependencies: + camera_platform_interface: ^2.0.1 + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.11.1 \ No newline at end of file diff --git a/packages/camera/camera_web/test/README.md b/packages/camera/camera_web/test/README.md new file mode 100644 index 000000000000..7c5b4ad682ba --- /dev/null +++ b/packages/camera/camera_web/test/README.md @@ -0,0 +1,5 @@ +## test + +This package uses integration tests for testing. + +See `example/README.md` for more info. diff --git a/packages/camera/camera_web/test/tests_exist_elsewhere_test.dart b/packages/camera/camera_web/test/tests_exist_elsewhere_test.dart new file mode 100644 index 000000000000..442c50144727 --- /dev/null +++ b/packages/camera/camera_web/test/tests_exist_elsewhere_test.dart @@ -0,0 +1,14 @@ +// 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:flutter_test/flutter_test.dart'; + +void main() { + test('Tell the user where to find the real tests', () { + print('---'); + print('This package uses integration_test for its tests.'); + print('See `example/README.md` for more info.'); + print('---'); + }); +} From 2f5ef3df0a31ebd23daefaeced3c60d13441543b Mon Sep 17 00:00:00 2001 From: Brett Morgan Date: Tue, 20 Jul 2021 04:25:33 +1000 Subject: [PATCH 008/123] [google_maps_flutter_web]: Update installation instructions (#4163) --- .../google_maps_flutter_web/CHANGELOG.md | 4 ++++ .../google_maps_flutter_web/README.md | 9 ++------- .../google_maps_flutter_web/pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md index 36a4271cb95d..d587c16f9207 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.0+3 + +* Update the `README.md` usage instructions to not be tied to explicit package versions. + ## 0.3.0+2 * Document `liteModeEnabled` is not available on the web. [#83737](https://github.com/flutter/flutter/issues/83737). diff --git a/packages/google_maps_flutter/google_maps_flutter_web/README.md b/packages/google_maps_flutter/google_maps_flutter_web/README.md index cfd5f6d8271e..9e7ce94e3e59 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/README.md @@ -6,13 +6,8 @@ This is an implementation of the [google_maps_flutter](https://pub.dev/packages/ ### Depend on the package -This package is not an endorsed implementation of the google_maps_flutter plugin yet, so you'll need to modify the `pubspec.yaml` file of your app to depend on this package: - -```yaml -dependencies: - google_maps_flutter: ^0.5.28 - google_maps_flutter_web: ^0.1.0 -``` +This package is not an endorsed implementation of the google_maps_flutter plugin yet, so you'll need to +[add it explicitly](https://pub.dev/packages/google_maps_flutter_web/install). ### Modify web/index.html diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index c69b8e55fa1c..c4323fc6486f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 0.3.0+2 +version: 0.3.0+3 environment: sdk: ">=2.12.0 <3.0.0" From e193daf1e1bee8627fbc758fbed6a922f7a5d5ee Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Mon, 19 Jul 2021 21:51:05 +0200 Subject: [PATCH 009/123] [camera] Add `CameraOptions` used to constrain the camera audio and video (#4164) --- packages/camera/camera_web/CHANGELOG.md | 1 + .../lib/src/types/camera_options.dart | 245 ++++++++++++++++++ .../camera_web/lib/src/types/types.dart | 5 + ...t => more_tests_exist_elsewhere_test.dart} | 4 +- .../test/types/camera_options_test.dart | 200 ++++++++++++++ 5 files changed, 453 insertions(+), 2 deletions(-) create mode 100644 packages/camera/camera_web/lib/src/types/camera_options.dart create mode 100644 packages/camera/camera_web/lib/src/types/types.dart rename packages/camera/camera_web/test/{tests_exist_elsewhere_test.dart => more_tests_exist_elsewhere_test.dart} (72%) create mode 100644 packages/camera/camera_web/test/types/camera_options_test.dart diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index 1318780830f8..68bc5f4e1a1e 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,3 +1,4 @@ ## 0.1.0 * Initial release + * Added CameraOptions used to constrain the camera audio and video. diff --git a/packages/camera/camera_web/lib/src/types/camera_options.dart b/packages/camera/camera_web/lib/src/types/camera_options.dart new file mode 100644 index 000000000000..2a4cdbf15348 --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/camera_options.dart @@ -0,0 +1,245 @@ +// 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 'dart:ui' show hashValues; + +/// Options used to create a camera with the given +/// [audio] and [video] media constraints. +/// +/// These options represent web `MediaStreamConstraints` +/// and can be used to request the browser for media streams +/// with audio and video tracks containing the requested types of media. +/// +/// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints +class CameraOptions { + /// Creates a new instance of [CameraOptions] + /// with the given [audio] and [video] constraints. + const CameraOptions({ + AudioConstraints? audio, + VideoConstraints? video, + }) : audio = audio ?? const AudioConstraints(), + video = video ?? const VideoConstraints(); + + /// The audio constraints for the camera. + final AudioConstraints audio; + + /// The video constraints for the camera. + final VideoConstraints video; + + /// Converts the current instance to a Map. + Map toJson() { + return { + 'audio': audio.toJson(), + 'video': video.toJson(), + }; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CameraOptions && + other.audio == audio && + other.video == video; + } + + @override + int get hashCode => hashValues(audio, video); +} + +/// Indicates whether the audio track is requested. +/// +/// By default, the audio track is not requested. +class AudioConstraints { + /// Creates a new instance of [AudioConstraints] + /// with the given [enabled] constraint. + const AudioConstraints({this.enabled = false}); + + /// Whether the audio track should be enabled. + final bool enabled; + + /// Converts the current instance to a Map. + Object toJson() => enabled; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is AudioConstraints && other.enabled == enabled; + } + + @override + int get hashCode => enabled.hashCode; +} + +/// Defines constraints that the video track must have +/// to be considered acceptable. +class VideoConstraints { + /// Creates a new instance of [VideoConstraints] + /// with the given constraints. + const VideoConstraints({ + this.facingMode, + this.width, + this.height, + this.deviceId, + }); + + /// The facing mode of the video track. + final FacingModeConstraint? facingMode; + + /// The width of the video track. + final VideoSizeConstraint? width; + + /// The height of the video track. + final VideoSizeConstraint? height; + + /// The device id of the video track. + final String? deviceId; + + /// Converts the current instance to a Map. + Object toJson() { + final json = {}; + + if (width != null) json['width'] = width!.toJson(); + if (height != null) json['height'] = height!.toJson(); + if (facingMode != null) json['facingMode'] = facingMode!.toJson(); + if (deviceId != null) json['deviceId'] = {'exact': deviceId!}; + + return json; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is VideoConstraints && + other.facingMode == facingMode && + other.width == width && + other.height == height && + other.deviceId == deviceId; + } + + @override + int get hashCode => hashValues(facingMode, width, height, deviceId); +} + +/// The camera type used in [FacingModeConstraint]. +/// +/// Specifies whether the requested camera should be facing away +/// or toward the user. +class CameraType { + const CameraType._(this._type); + + final String _type; + + @override + String toString() => _type; + + /// The camera is facing away from the user, viewing their environment. + /// This includes the back camera on a smartphone. + static const CameraType environment = CameraType._('environment'); + + /// The camera is facing toward the user. + /// This includes the front camera on a smartphone. + static const CameraType user = CameraType._('user'); +} + +/// Indicates the direction in which the desired camera should be pointing. +class FacingModeConstraint { + /// Creates a new instance of [FacingModeConstraint] + /// with the given [ideal] and [exact] constraints. + const FacingModeConstraint._({this.ideal, this.exact}); + + /// Creates a new instance of [FacingModeConstraint] + /// with [ideal] constraint set to [type]. + factory FacingModeConstraint(CameraType type) => + FacingModeConstraint._(ideal: type); + + /// Creates a new instance of [FacingModeConstraint] + /// with [exact] constraint set to [type]. + factory FacingModeConstraint.exact(CameraType type) => + FacingModeConstraint._(exact: type); + + /// The ideal facing mode constraint. + /// + /// If this constraint is used, then the camera would ideally have + /// the desired facing [type] but it may be considered optional. + final CameraType? ideal; + + /// The exact facing mode constraint. + /// + /// If this constraint is used, then the camera must have + /// the desired facing [type] to be considered acceptable. + final CameraType? exact; + + /// Converts the current instance to a Map. + Object? toJson() { + return { + if (ideal != null) 'ideal': ideal.toString(), + if (exact != null) 'exact': exact.toString(), + }; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is FacingModeConstraint && + other.ideal == ideal && + other.exact == exact; + } + + @override + int get hashCode => hashValues(ideal, exact); +} + +/// The size of the requested video track used in +/// [VideoConstraints.width] and [VideoConstraints.height]. +/// +/// The obtained video track will have a size between [minimum] and [maximum] +/// with ideally a size of [ideal]. The size is determined by +/// the capabilities of the hardware and the other specified constraints. +class VideoSizeConstraint { + /// Creates a new instance of [VideoSizeConstraint] with the given + /// [minimum], [ideal] and [maximum] constraints. + const VideoSizeConstraint({this.minimum, this.ideal, this.maximum}); + + /// The minimum video size. + final int? minimum; + + /// The ideal video size. + /// + /// The video would ideally have the [ideal] size + /// but it may be considered optional. If not possible + /// to satisfy, the size will be as close as possible + /// to [ideal]. + final int? ideal; + + /// The maximum video size. + final int? maximum; + + /// Converts the current instance to a Map. + Object toJson() { + final json = {}; + + if (ideal != null) json['ideal'] = ideal; + if (minimum != null) json['min'] = minimum; + if (maximum != null) json['max'] = maximum; + + return json; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is VideoSizeConstraint && + other.minimum == minimum && + other.ideal == ideal && + other.maximum == maximum; + } + + @override + int get hashCode => hashValues(minimum, ideal, maximum); +} diff --git a/packages/camera/camera_web/lib/src/types/types.dart b/packages/camera/camera_web/lib/src/types/types.dart new file mode 100644 index 000000000000..deccd32da4c0 --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/types.dart @@ -0,0 +1,5 @@ +// 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. + +export 'camera_options.dart'; diff --git a/packages/camera/camera_web/test/tests_exist_elsewhere_test.dart b/packages/camera/camera_web/test/more_tests_exist_elsewhere_test.dart similarity index 72% rename from packages/camera/camera_web/test/tests_exist_elsewhere_test.dart rename to packages/camera/camera_web/test/more_tests_exist_elsewhere_test.dart index 442c50144727..dc2b64c111d7 100644 --- a/packages/camera/camera_web/test/tests_exist_elsewhere_test.dart +++ b/packages/camera/camera_web/test/more_tests_exist_elsewhere_test.dart @@ -5,9 +5,9 @@ import 'package:flutter_test/flutter_test.dart'; void main() { - test('Tell the user where to find the real tests', () { + test('Tell the user where to find more tests', () { print('---'); - print('This package uses integration_test for its tests.'); + print('This package also uses integration_test for its tests.'); print('See `example/README.md` for more info.'); print('---'); }); diff --git a/packages/camera/camera_web/test/types/camera_options_test.dart b/packages/camera/camera_web/test/types/camera_options_test.dart new file mode 100644 index 000000000000..6f60bfd5aeda --- /dev/null +++ b/packages/camera/camera_web/test/types/camera_options_test.dart @@ -0,0 +1,200 @@ +// 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:camera_web/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('CameraOptions', () { + test('serializes correctly', () { + final cameraOptions = CameraOptions( + audio: AudioConstraints(enabled: true), + video: VideoConstraints( + facingMode: FacingModeConstraint.exact(CameraType.user), + ), + ); + + expect( + cameraOptions.toJson(), + equals({ + 'audio': cameraOptions.audio.toJson(), + 'video': cameraOptions.video.toJson(), + }), + ); + }); + + test('supports value equality', () { + expect( + CameraOptions( + audio: AudioConstraints(enabled: false), + video: VideoConstraints( + facingMode: FacingModeConstraint(CameraType.environment), + width: VideoSizeConstraint(minimum: 10, ideal: 15, maximum: 20), + height: VideoSizeConstraint(minimum: 15, ideal: 20, maximum: 25), + deviceId: 'deviceId', + ), + ), + equals( + CameraOptions( + audio: AudioConstraints(enabled: false), + video: VideoConstraints( + facingMode: FacingModeConstraint(CameraType.environment), + width: VideoSizeConstraint(minimum: 10, ideal: 15, maximum: 20), + height: VideoSizeConstraint(minimum: 15, ideal: 20, maximum: 25), + deviceId: 'deviceId', + ), + ), + ), + ); + }); + }); + + group('AudioConstraints', () { + test('serializes correctly', () { + expect( + AudioConstraints(enabled: true).toJson(), + equals(true), + ); + }); + + test('supports value equality', () { + expect( + AudioConstraints(enabled: true), + equals(AudioConstraints(enabled: true)), + ); + }); + }); + + group('VideoConstraints', () { + test('serializes correctly', () { + final videoConstraints = VideoConstraints( + facingMode: FacingModeConstraint.exact(CameraType.user), + width: VideoSizeConstraint(ideal: 100, maximum: 100), + height: VideoSizeConstraint(ideal: 50, maximum: 50), + deviceId: 'deviceId', + ); + + expect( + videoConstraints.toJson(), + equals({ + 'facingMode': videoConstraints.facingMode!.toJson(), + 'width': videoConstraints.width!.toJson(), + 'height': videoConstraints.height!.toJson(), + 'deviceId': { + 'exact': 'deviceId', + } + }), + ); + }); + + test('supports value equality', () { + expect( + VideoConstraints( + facingMode: FacingModeConstraint.exact(CameraType.environment), + width: VideoSizeConstraint(minimum: 90, ideal: 100, maximum: 100), + height: VideoSizeConstraint(minimum: 40, ideal: 50, maximum: 50), + deviceId: 'deviceId', + ), + equals( + VideoConstraints( + facingMode: FacingModeConstraint.exact(CameraType.environment), + width: VideoSizeConstraint(minimum: 90, ideal: 100, maximum: 100), + height: VideoSizeConstraint(minimum: 40, ideal: 50, maximum: 50), + deviceId: 'deviceId', + ), + ), + ); + }); + }); + + group('FacingModeConstraint', () { + group('ideal', () { + test( + 'serializes correctly ' + 'for environment camera type', () { + expect( + FacingModeConstraint(CameraType.environment).toJson(), + equals({'ideal': 'environment'}), + ); + }); + + test( + 'serializes correctly ' + 'for user camera type', () { + expect( + FacingModeConstraint(CameraType.user).toJson(), + equals({'ideal': 'user'}), + ); + }); + + test('supports value equality', () { + expect( + FacingModeConstraint(CameraType.user), + equals(FacingModeConstraint(CameraType.user)), + ); + }); + }); + + group('exact', () { + test( + 'serializes correctly ' + 'for environment camera type', () { + expect( + FacingModeConstraint.exact(CameraType.environment).toJson(), + equals({'exact': 'environment'}), + ); + }); + + test( + 'serializes correctly ' + 'for user camera type', () { + expect( + FacingModeConstraint.exact(CameraType.user).toJson(), + equals({'exact': 'user'}), + ); + }); + + test('supports value equality', () { + expect( + FacingModeConstraint.exact(CameraType.environment), + equals(FacingModeConstraint.exact(CameraType.environment)), + ); + }); + }); + }); + + group('VideoSizeConstraint ', () { + test('serializes correctly', () { + expect( + VideoSizeConstraint( + minimum: 200, + ideal: 400, + maximum: 400, + ).toJson(), + equals({ + 'min': 200, + 'ideal': 400, + 'max': 400, + }), + ); + }); + + test('supports value equality', () { + expect( + VideoSizeConstraint( + minimum: 100, + ideal: 200, + maximum: 300, + ), + equals( + VideoSizeConstraint( + minimum: 100, + ideal: 200, + maximum: 300, + ), + ), + ); + }); + }); +} From 027db9039c0364cc75273480602d70b58df68a53 Mon Sep 17 00:00:00 2001 From: Nishant Chandla Date: Tue, 20 Jul 2021 03:01:04 +0530 Subject: [PATCH 010/123] [multiple_web] Update web plugin testing instructions. (#4167) --- .../connectivity_for_web/example/README.md | 22 +++---------- .../file_selector_web/example/README.md | 22 +++---------- .../google_maps_flutter_web/example/README.md | 33 ++++--------------- .../google_sign_in_web/example/README.md | 22 +++---------- .../url_launcher_web/example/README.md | 33 ++++--------------- .../video_player_web/example/README.md | 22 +++---------- 6 files changed, 34 insertions(+), 120 deletions(-) diff --git a/packages/connectivity/connectivity_for_web/example/README.md b/packages/connectivity/connectivity_for_web/example/README.md index 0ec01e025570..8a6e74b107ea 100644 --- a/packages/connectivity/connectivity_for_web/example/README.md +++ b/packages/connectivity/connectivity_for_web/example/README.md @@ -1,21 +1,9 @@ # Testing -This package utilizes the `integration_test` package to run its tests in a web browser. +This package uses `package:integration_test` to run its tests in a web browser. -See [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. -## Running the tests - -Make sure you have updated to the latest Flutter master. - -1. Check what version of Chrome is running on the machine you're running tests on. - -2. Download and install driver for that version from here: - * - -3. Start the driver using `chromedriver --port=4444` - -4. Run tests: `flutter drive -d web-server --browser-name=chrome --driver=test_driver/integration_driver.dart --target=integration_test/TEST_NAME.dart`, or (in Linux): - - * Single: `./run_test.sh integration_test/TEST_NAME.dart` - * All: `./run_test.sh` +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. \ No newline at end of file diff --git a/packages/file_selector/file_selector_web/example/README.md b/packages/file_selector/file_selector_web/example/README.md index 6187e55841c9..8a6e74b107ea 100644 --- a/packages/file_selector/file_selector_web/example/README.md +++ b/packages/file_selector/file_selector_web/example/README.md @@ -1,21 +1,9 @@ # Testing -This package utilizes the `integration_test` package to run its tests in a web browser. +This package uses `package:integration_test` to run its tests in a web browser. -See [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. -## Running the tests - -Make sure you have updated to the latest Flutter master. - -1. Check what version of Chrome is running on the machine you're running tests on. - -2. Download and install driver for that version from here: - * - -3. Start the driver using `chromedriver --port=4444` - -4. Run tests: `flutter drive -d web-server --browser-name=chrome --driver=test_driver/integration_test.dart --target=integration_test/TEST_NAME.dart`, or (in Linux): - - * Single: `./run_test.sh integration_test/TEST_NAME.dart` - * All: `./run_test.sh` +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. \ No newline at end of file diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/README.md b/packages/google_maps_flutter/google_maps_flutter_web/example/README.md index 582288a561a4..3cdecfab2ab9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/README.md @@ -1,31 +1,12 @@ # Testing -This package utilizes the `integration_test` package to run its tests in a web browser. +This package uses `package:integration_test` to run its tests in a web browser. -See [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. -## Running the tests +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. -Make sure you have updated to the latest Flutter master. - -1. Check what version of Chrome is running on the machine you're running tests on. - -2. Download and install driver for that version from here: - * - -3. Start the driver using `chromedriver --port=4444` - -4. Run tests: `flutter drive -d web-server --browser-name=chrome --driver=test_driver/integration_driver.dart --target=integration_test/TEST_NAME.dart`, or (in Linux): - - * Single: `./run_test.sh integration_test/TEST_NAME.dart` - * All: `./run_test.sh` - -## Mocks - -There's new `.mocks.dart` files next to the test files that use them. - -Mock files are [generated by `package:mockito`](https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#code-generation). The contents of these files can change with how the mocks are used within the tests, in addition to actual changes in the APIs they're mocking. - -Mock files can be updated either manually by running the following command: `flutter pub run build_runner build` (or the `regen_mocks.sh` script), or automatically on each call to the `run_test.sh` script. - -Please, add whatever changes show up in mock files to your PRs, or CI will fail. +See [Plugin Tests > Web Tests > Mocks](https://github.com/flutter/flutter/wiki/Plugin-Tests#mocks) +in the Flutter wiki for more information about the `.mocks.dart` files in this package. \ No newline at end of file diff --git a/packages/google_sign_in/google_sign_in_web/example/README.md b/packages/google_sign_in/google_sign_in_web/example/README.md index 0ec01e025570..8a6e74b107ea 100644 --- a/packages/google_sign_in/google_sign_in_web/example/README.md +++ b/packages/google_sign_in/google_sign_in_web/example/README.md @@ -1,21 +1,9 @@ # Testing -This package utilizes the `integration_test` package to run its tests in a web browser. +This package uses `package:integration_test` to run its tests in a web browser. -See [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. -## Running the tests - -Make sure you have updated to the latest Flutter master. - -1. Check what version of Chrome is running on the machine you're running tests on. - -2. Download and install driver for that version from here: - * - -3. Start the driver using `chromedriver --port=4444` - -4. Run tests: `flutter drive -d web-server --browser-name=chrome --driver=test_driver/integration_driver.dart --target=integration_test/TEST_NAME.dart`, or (in Linux): - - * Single: `./run_test.sh integration_test/TEST_NAME.dart` - * All: `./run_test.sh` +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. \ No newline at end of file diff --git a/packages/url_launcher/url_launcher_web/example/README.md b/packages/url_launcher/url_launcher_web/example/README.md index b75df09c33f1..3cdecfab2ab9 100644 --- a/packages/url_launcher/url_launcher_web/example/README.md +++ b/packages/url_launcher/url_launcher_web/example/README.md @@ -1,31 +1,12 @@ # Testing -This package utilizes the `integration_test` package to run its tests in a web browser. +This package uses `package:integration_test` to run its tests in a web browser. -See [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. -## Running the tests +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. -Make sure you have updated to the latest Flutter master. - -1. Check what version of Chrome is running on the machine you're running tests on. - -2. Download and install driver for that version from here: - * - -3. Start the driver using `chromedriver --port=4444` - -4. Run tests: `flutter drive -d web-server --browser-name=chrome --driver=test_driver/integration_test_driver.dart --target=integration_test/TEST_NAME.dart`, or (in Linux): - - * Single: `./run_test.sh integration_test/TEST_NAME.dart` - * All: `./run_test.sh` - -## Mocks - -There's `.mocks.dart` files next to the test files that use them. - -They're [generated by Mockito](https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#code-generation). - -Mocks might be manually re-generated with the following command: `flutter pub run build_runner build`. If there are any changes in the mocks, feel free to commit them. - -(Mocks will be auto-generated by the `run_test.sh` script as well.) +See [Plugin Tests > Web Tests > Mocks](https://github.com/flutter/flutter/wiki/Plugin-Tests#mocks) +in the Flutter wiki for more information about the `.mocks.dart` files in this package. \ No newline at end of file diff --git a/packages/video_player/video_player_web/example/README.md b/packages/video_player/video_player_web/example/README.md index 0ec01e025570..8a6e74b107ea 100644 --- a/packages/video_player/video_player_web/example/README.md +++ b/packages/video_player/video_player_web/example/README.md @@ -1,21 +1,9 @@ # Testing -This package utilizes the `integration_test` package to run its tests in a web browser. +This package uses `package:integration_test` to run its tests in a web browser. -See [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. -## Running the tests - -Make sure you have updated to the latest Flutter master. - -1. Check what version of Chrome is running on the machine you're running tests on. - -2. Download and install driver for that version from here: - * - -3. Start the driver using `chromedriver --port=4444` - -4. Run tests: `flutter drive -d web-server --browser-name=chrome --driver=test_driver/integration_driver.dart --target=integration_test/TEST_NAME.dart`, or (in Linux): - - * Single: `./run_test.sh integration_test/TEST_NAME.dart` - * All: `./run_test.sh` +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. \ No newline at end of file From 45a1ba6d91af70b8ad6d0c036f2ed3f60f72a357 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Mon, 19 Jul 2021 17:05:00 -0700 Subject: [PATCH 011/123] [google_sign_in] Add iOS unit tests (#4157) --- .../google_sign_in/CHANGELOG.md | 3 +- .../ios/Flutter/AppFrameworkInfo.plist | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 4 +- .../ios/RunnerTests/GoogleSignInTests.m | 417 ++++++++++++++++-- .../ios/Classes/FLTGoogleSignInPlugin.m | 72 +-- .../Classes/FLTGoogleSignInPlugin.modulemap | 10 + .../ios/Classes/FLTGoogleSignInPlugin_Test.h | 17 + .../ios/Classes/google_sign_in-umbrella.h | 9 + .../google_sign_in/ios/google_sign_in.podspec | 3 +- .../google_sign_in/pubspec.yaml | 2 +- 10 files changed, 462 insertions(+), 77 deletions(-) create mode 100644 packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.modulemap create mode 100644 packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin_Test.h create mode 100644 packages/google_sign_in/google_sign_in/ios/Classes/google_sign_in-umbrella.h diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index cb4a65f42fa2..186a1d39a223 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 5.0.5 * Add iOS unit and UI integration test targets. +* Add iOS unit test module map. * Exclude arm64 simulators in example app. ## 5.0.4 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Flutter/AppFrameworkInfo.plist b/packages/google_sign_in/google_sign_in/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/google_sign_in/google_sign_in/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/google_sign_in/google_sign_in/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj index 0c3cc430d23e..06857ed2bd59 100644 --- a/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj @@ -539,7 +539,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -589,7 +589,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/GoogleSignInTests.m b/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/GoogleSignInTests.m index adbf61326c8d..6f8b821a5299 100644 --- a/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/GoogleSignInTests.m +++ b/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/GoogleSignInTests.m @@ -6,6 +6,7 @@ @import XCTest; @import google_sign_in; +@import google_sign_in.Test; @import GoogleSignIn; // OCMock library doesn't generate a valid modulemap. @@ -16,7 +17,7 @@ @interface FLTGoogleSignInPluginTest : XCTestCase @property(strong, nonatomic) NSObject *mockBinaryMessenger; @property(strong, nonatomic) NSObject *mockPluginRegistrar; @property(strong, nonatomic) FLTGoogleSignInPlugin *plugin; -@property(strong, nonatomic) GIDSignIn *mockSharedInstance; +@property(strong, nonatomic) id mockSignIn; @end @@ -26,39 +27,377 @@ - (void)setUp { [super setUp]; self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); self.mockPluginRegistrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); - self.mockSharedInstance = [OCMockObject partialMockForObject:[GIDSignIn sharedInstance]]; + + id mockSignIn = OCMClassMock([GIDSignIn class]); + self.mockSignIn = mockSignIn; + OCMStub(self.mockPluginRegistrar.messenger).andReturn(self.mockBinaryMessenger); - self.plugin = [[FLTGoogleSignInPlugin alloc] init]; + self.plugin = [[FLTGoogleSignInPlugin alloc] initWithSignIn:mockSignIn]; [FLTGoogleSignInPlugin registerWithRegistrar:self.mockPluginRegistrar]; } -- (void)tearDown { - [((OCMockObject *)self.mockSharedInstance) stopMocking]; - [super tearDown]; +- (void)testUnimplementedMethod { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"bogus" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(id result) { + XCTAssertEqualObjects(result, FlutterMethodNotImplemented); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testSignOut { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signOut" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(id result) { + XCTAssertNil(result); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerify([self.mockSignIn signOut]); +} + +- (void)testDisconnect { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"disconnect" + arguments:nil]; + + [self.plugin handleMethodCall:methodCall + result:^(id result){ + }]; + OCMVerify([self.mockSignIn disconnect]); +} + +- (void)testClearAuthCache { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"clearAuthCache" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(id result) { + XCTAssertNil(result); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +#pragma mark - Init + +- (void)testInitGamesSignInUnsupported { + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"init" + arguments:@{@"signInOption" : @"SignInOption.games"}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"unsupported-options"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testInitGoogleServiceInfoPlist { + FlutterMethodCall *methodCall = [FlutterMethodCall + methodCallWithMethodName:@"init" + arguments:@{@"scopes" : @[ @"mockScope1" ], @"hostedDomain" : @"example.com"}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(id result) { + XCTAssertNil(result); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + + id mockSignIn = self.mockSignIn; + OCMVerify([mockSignIn setScopes:@[ @"mockScope1" ]]); + OCMVerify([mockSignIn setHostedDomain:@"example.com"]); + + // Set in example app GoogleService-Info.plist. + OCMVerify([mockSignIn + setClientID:@"479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com"]); + OCMVerify([mockSignIn setServerClientID:@"YOUR_SERVER_CLIENT_ID"]); +} + +- (void)testInitNullDomain { + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"init" + arguments:@{@"hostedDomain" : [NSNull null]}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(id r) { + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerify([self.mockSignIn setHostedDomain:nil]); +} + +- (void)testInitDynamicClientId { + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"init" + arguments:@{@"clientId" : @"mockClientId"}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(id r) { + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerify([self.mockSignIn setClientID:@"mockClientId"]); +} + +#pragma mark - Is signed in + +- (void)testIsNotSignedIn { + OCMStub([self.mockSignIn hasPreviousSignIn]).andReturn(NO); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"isSignedIn" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSNumber *result) { + XCTAssertFalse(result.boolValue); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testIsSignedIn { + OCMStub([self.mockSignIn hasPreviousSignIn]).andReturn(YES); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"isSignedIn" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSNumber *result) { + XCTAssertTrue(result.boolValue); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +#pragma mark - Sign in silently + +- (void)testSignInSilently { + OCMExpect([self.mockSignIn restorePreviousSignIn]); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signInSilently" + arguments:nil]; + + [self.plugin handleMethodCall:methodCall + result:^(id result){ + }]; + OCMVerifyAll(self.mockSignIn); +} + +- (void)testSignInSilentlyFailsConcurrently { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signInSilently" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + + OCMExpect([self.mockSignIn restorePreviousSignIn]).andDo(^(NSInvocation *invocation) { + // Simulate calling the same method while the previous one is in flight. + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"concurrent-requests"); + [expectation fulfill]; + }]; + }); + + [self.plugin handleMethodCall:methodCall + result:^(id result){ + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +#pragma mark - Sign in + +- (void)testSignIn { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + + [self.plugin handleMethodCall:methodCall + result:^(NSNumber *result){ + }]; + + id mockSignIn = self.mockSignIn; + OCMVerify([mockSignIn + setPresentingViewController:[OCMArg isKindOfClass:[FlutterViewController class]]]); + OCMVerify([mockSignIn signIn]); +} + +- (void)testSignInExecption { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + OCMExpect([self.mockSignIn signIn]) + .andThrow([NSException exceptionWithName:@"MockName" reason:@"MockReason" userInfo:nil]); + + __block FlutterError *error; + XCTAssertThrows([self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + error = result; + }]); + + XCTAssertEqualObjects(error.code, @"google_sign_in"); + XCTAssertEqualObjects(error.message, @"MockReason"); + XCTAssertEqualObjects(error.details, @"MockName"); +} + +#pragma mark - Get tokens + +- (void)testGetTokens { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + + id mockAuthentication = OCMClassMock([GIDAuthentication class]); + OCMStub([mockAuthentication idToken]).andReturn(@"mockIdToken"); + OCMStub([mockAuthentication accessToken]).andReturn(@"mockAccessToken"); + [[mockAuthentication stub] + getTokensWithHandler:[OCMArg invokeBlockWithArgs:mockAuthentication, [NSNull null], nil]]; + OCMStub([mockUser authentication]).andReturn(mockAuthentication); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSDictionary *result) { + XCTAssertEqualObjects(result[@"idToken"], @"mockIdToken"); + XCTAssertEqualObjects(result[@"accessToken"], @"mockAccessToken"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; } +- (void)testGetTokensNoAuthKeychainError { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + + id mockAuthentication = OCMClassMock([GIDAuthentication class]); + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeHasNoAuthInKeychain + userInfo:nil]; + [[mockAuthentication stub] + getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + OCMStub([mockUser authentication]).andReturn(mockAuthentication); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_required"); + XCTAssertEqualObjects(result.message, kGIDSignInErrorDomain); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testGetTokensCancelledError { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + + id mockAuthentication = OCMClassMock([GIDAuthentication class]); + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeCanceled + userInfo:nil]; + [[mockAuthentication stub] + getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + OCMStub([mockUser authentication]).andReturn(mockAuthentication); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_canceled"); + XCTAssertEqualObjects(result.message, kGIDSignInErrorDomain); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testGetTokensURLError { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + + id mockAuthentication = OCMClassMock([GIDAuthentication class]); + NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:nil]; + [[mockAuthentication stub] + getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + OCMStub([mockUser authentication]).andReturn(mockAuthentication); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"network_error"); + XCTAssertEqualObjects(result.message, NSURLErrorDomain); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testGetTokensUnknownError { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + + id mockAuthentication = OCMClassMock([GIDAuthentication class]); + NSError *error = [NSError errorWithDomain:@"BogusDomain" code:42 userInfo:nil]; + [[mockAuthentication stub] + getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + OCMStub([mockUser authentication]).andReturn(mockAuthentication); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_failed"); + XCTAssertEqualObjects(result.message, @"BogusDomain"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +#pragma mark - Request scopes + - (void)testRequestScopesResultErrorIfNotSignedIn { - OCMStub(self.mockSharedInstance.currentUser).andReturn(nil); + OCMStub([self.mockSignIn currentUser]).andReturn(nil); FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"requestScopes" arguments:@{@"scopes" : @[ @"mockScope1" ]}]; XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - __block id result; [self.plugin handleMethodCall:methodCall - result:^(id r) { + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_required"); [expectation fulfill]; - result = r; }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqualObjects([((FlutterError *)result) code], @"sign_in_required"); + [self waitForExpectationsWithTimeout:5.0 handler:nil]; } - (void)testRequestScopesIfNoMissingScope { // Mock Google Signin internal calls - GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); - OCMStub(self.mockSharedInstance.currentUser).andReturn(mockUser); + GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); NSArray *requestedScopes = @[ @"mockScope1" ]; OCMStub(mockUser.grantedScopes).andReturn(requestedScopes); FlutterMethodCall *methodCall = @@ -66,22 +405,22 @@ - (void)testRequestScopesIfNoMissingScope { arguments:@{@"scopes" : requestedScopes}]; XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - __block id result; [self.plugin handleMethodCall:methodCall - result:^(id r) { + result:^(NSNumber *result) { + XCTAssertTrue(result.boolValue); [expectation fulfill]; - result = r; }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertTrue([result boolValue]); + [self waitForExpectationsWithTimeout:5.0 handler:nil]; } - (void)testRequestScopesRequestsIfNotGranted { // Mock Google Signin internal calls - GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); - OCMStub(self.mockSharedInstance.currentUser).andReturn(mockUser); + GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); NSArray *requestedScopes = @[ @"mockScope1" ]; OCMStub(mockUser.grantedScopes).andReturn(@[]); + id mockSignIn = self.mockSignIn; + OCMStub([mockSignIn scopes]).andReturn(@[]); FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"requestScopes" @@ -91,19 +430,19 @@ - (void)testRequestScopesRequestsIfNotGranted { result:^(id r){ }]; - XCTAssertTrue([self.mockSharedInstance.scopes containsObject:@"mockScope1"]); - OCMVerify([self.mockSharedInstance signIn]); + OCMVerify([mockSignIn setScopes:@[ @"mockScope1" ]]); + OCMVerify([mockSignIn signIn]); } - (void)testRequestScopesReturnsFalseIfNotGranted { // Mock Google Signin internal calls - GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); - OCMStub(self.mockSharedInstance.currentUser).andReturn(mockUser); + GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); NSArray *requestedScopes = @[ @"mockScope1" ]; OCMStub(mockUser.grantedScopes).andReturn(@[]); - OCMStub([self.mockSharedInstance signIn]).andDo(^(NSInvocation *invocation) { - [((NSObject *)self.plugin) signIn:self.mockSharedInstance + OCMStub([self.mockSignIn signIn]).andDo(^(NSInvocation *invocation) { + [((NSObject *)self.plugin) signIn:self.mockSignIn didSignInForUser:mockUser withError:nil]; }); @@ -113,27 +452,25 @@ - (void)testRequestScopesReturnsFalseIfNotGranted { arguments:@{@"scopes" : requestedScopes}]; XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns false"]; - __block id result; [self.plugin handleMethodCall:methodCall - result:^(id r) { + result:^(NSNumber *result) { + XCTAssertFalse(result.boolValue); [expectation fulfill]; - result = r; }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertFalse([result boolValue]); + [self waitForExpectationsWithTimeout:5.0 handler:nil]; } - (void)testRequestScopesReturnsTrueIfGranted { // Mock Google Signin internal calls - GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); - OCMStub(self.mockSharedInstance.currentUser).andReturn(mockUser); + GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); NSArray *requestedScopes = @[ @"mockScope1" ]; NSMutableArray *availableScopes = [NSMutableArray new]; OCMStub(mockUser.grantedScopes).andReturn(availableScopes); - OCMStub([self.mockSharedInstance signIn]).andDo(^(NSInvocation *invocation) { + OCMStub([self.mockSignIn signIn]).andDo(^(NSInvocation *invocation) { [availableScopes addObject:@"mockScope1"]; - [((NSObject *)self.plugin) signIn:self.mockSharedInstance + [((NSObject *)self.plugin) signIn:self.mockSignIn didSignInForUser:mockUser withError:nil]; }); @@ -143,14 +480,12 @@ - (void)testRequestScopesReturnsTrueIfGranted { arguments:@{@"scopes" : requestedScopes}]; XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - __block id result; [self.plugin handleMethodCall:methodCall - result:^(id r) { + result:^(NSNumber *result) { + XCTAssertTrue(result.boolValue); [expectation fulfill]; - result = r; }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertTrue([result boolValue]); + [self waitForExpectationsWithTimeout:5.0 handler:nil]; } @end diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m index 578f64d5a41c..d13d64d2ba04 100644 --- a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m +++ b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m @@ -3,6 +3,8 @@ // found in the LICENSE file. #import "FLTGoogleSignInPlugin.h" +#import "FLTGoogleSignInPlugin_Test.h" + #import // The key within `GoogleService-Info.plist` used to hold the application's @@ -35,11 +37,15 @@ } @interface FLTGoogleSignInPlugin () +@property(strong, readonly) GIDSignIn *signIn; + +// Redeclared as not a designated initializer. +- (instancetype)init; @end @implementation FLTGoogleSignInPlugin { FlutterResult _accountRequest; - NSArray *_additionalScopesRequest; + NSArray *_additionalScopesRequest; } + (void)registerWithRegistrar:(NSObject *)registrar { @@ -52,9 +58,14 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } - (instancetype)init { + return [self initWithSignIn:GIDSignIn.sharedInstance]; +} + +- (instancetype)initWithSignIn:(GIDSignIn *)signIn { self = [super init]; if (self) { - [GIDSignIn sharedInstance].delegate = self; + _signIn = signIn; + _signIn.delegate = self; // On the iOS simulator, we get "Broken pipe" errors after sign-in for some // unknown reason. We can avoid crashing the app by ignoring them. @@ -76,22 +87,22 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result NSString *path = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" ofType:@"plist"]; if (path) { - NSMutableDictionary *plist = [[NSMutableDictionary alloc] initWithContentsOfFile:path]; - BOOL hasDynamicClientId = - [[call.arguments valueForKey:@"clientId"] isKindOfClass:[NSString class]]; + NSMutableDictionary *plist = + [[NSMutableDictionary alloc] initWithContentsOfFile:path]; + BOOL hasDynamicClientId = [call.arguments[@"clientId"] isKindOfClass:[NSString class]]; if (hasDynamicClientId) { - [GIDSignIn sharedInstance].clientID = [call.arguments valueForKey:@"clientId"]; + self.signIn.clientID = call.arguments[@"clientId"]; } else { - [GIDSignIn sharedInstance].clientID = plist[kClientIdKey]; + self.signIn.clientID = plist[kClientIdKey]; } - [GIDSignIn sharedInstance].serverClientID = plist[kServerClientIdKey]; - [GIDSignIn sharedInstance].scopes = call.arguments[@"scopes"]; + self.signIn.serverClientID = plist[kServerClientIdKey]; + self.signIn.scopes = call.arguments[@"scopes"]; if (call.arguments[@"hostedDomain"] == [NSNull null]) { - [GIDSignIn sharedInstance].hostedDomain = nil; + self.signIn.hostedDomain = nil; } else { - [GIDSignIn sharedInstance].hostedDomain = call.arguments[@"hostedDomain"]; + self.signIn.hostedDomain = call.arguments[@"hostedDomain"]; } result(nil); } else { @@ -102,23 +113,23 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } } else if ([call.method isEqualToString:@"signInSilently"]) { if ([self setAccountRequest:result]) { - [[GIDSignIn sharedInstance] restorePreviousSignIn]; + [self.signIn restorePreviousSignIn]; } } else if ([call.method isEqualToString:@"isSignedIn"]) { - result(@([[GIDSignIn sharedInstance] hasPreviousSignIn])); + result(@([self.signIn hasPreviousSignIn])); } else if ([call.method isEqualToString:@"signIn"]) { - [GIDSignIn sharedInstance].presentingViewController = [self topViewController]; + self.signIn.presentingViewController = [self topViewController]; if ([self setAccountRequest:result]) { @try { - [[GIDSignIn sharedInstance] signIn]; + [self.signIn signIn]; } @catch (NSException *e) { result([FlutterError errorWithCode:@"google_sign_in" message:e.reason details:e.name]); [e raise]; } } } else if ([call.method isEqualToString:@"getTokens"]) { - GIDGoogleUser *currentUser = [GIDSignIn sharedInstance].currentUser; + GIDGoogleUser *currentUser = self.signIn.currentUser; GIDAuthentication *auth = currentUser.authentication; [auth getTokensWithHandler:^void(GIDAuthentication *authentication, NSError *error) { result(error != nil ? getFlutterError(error) : @{ @@ -127,18 +138,18 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result }); }]; } else if ([call.method isEqualToString:@"signOut"]) { - [[GIDSignIn sharedInstance] signOut]; + [self.signIn signOut]; result(nil); } else if ([call.method isEqualToString:@"disconnect"]) { if ([self setAccountRequest:result]) { - [[GIDSignIn sharedInstance] disconnect]; + [self.signIn disconnect]; } } else if ([call.method isEqualToString:@"clearAuthCache"]) { // There's nothing to be done here on iOS since the expired/invalid // tokens are refreshed automatically by getTokensWithHandler. result(nil); } else if ([call.method isEqualToString:@"requestScopes"]) { - GIDGoogleUser *user = [GIDSignIn sharedInstance].currentUser; + GIDGoogleUser *user = self.signIn.currentUser; if (user == nil) { result([FlutterError errorWithCode:@"sign_in_required" message:@"No account to grant scopes." @@ -146,9 +157,9 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result return; } - NSArray *currentScopes = [GIDSignIn sharedInstance].scopes; - NSArray *scopes = call.arguments[@"scopes"]; - NSArray *missingScopes = [scopes + NSArray *currentScopes = self.signIn.scopes; + NSArray *scopes = call.arguments[@"scopes"]; + NSArray *missingScopes = [scopes filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id scope, NSDictionary *bindings) { return ![user.grantedScopes containsObject:scope]; @@ -161,12 +172,11 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result if ([self setAccountRequest:result]) { _additionalScopesRequest = missingScopes; - [GIDSignIn sharedInstance].scopes = - [currentScopes arrayByAddingObjectsFromArray:missingScopes]; - [GIDSignIn sharedInstance].presentingViewController = [self topViewController]; - [GIDSignIn sharedInstance].loginHint = user.profile.email; + self.signIn.scopes = [currentScopes arrayByAddingObjectsFromArray:missingScopes]; + self.signIn.presentingViewController = [self topViewController]; + self.signIn.loginHint = user.profile.email; @try { - [[GIDSignIn sharedInstance] signIn]; + [self.signIn signIn]; } @catch (NSException *e) { result([FlutterError errorWithCode:@"request_scopes" message:e.reason details:e.name]); } @@ -187,8 +197,10 @@ - (BOOL)setAccountRequest:(FlutterResult)request { return YES; } -- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [[GIDSignIn sharedInstance] handleURL:url]; +- (BOOL)application:(UIApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options { + return [self.signIn handleURL:url]; } #pragma mark - protocol @@ -251,7 +263,7 @@ - (void)signIn:(GIDSignIn *)signIn #pragma mark - private methods -- (void)respondWithAccount:(id)account error:(NSError *)error { +- (void)respondWithAccount:(NSDictionary *)account error:(NSError *)error { FlutterResult result = _accountRequest; _accountRequest = nil; result(error != nil ? getFlutterError(error) : account); diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.modulemap b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.modulemap new file mode 100644 index 000000000000..271f509e7fd7 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.modulemap @@ -0,0 +1,10 @@ +framework module google_sign_in { + umbrella header "google_sign_in-umbrella.h" + + export * + module * { export * } + + explicit module Test { + header "FLTGoogleSignInPlugin_Test.h" + } +} diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin_Test.h b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin_Test.h new file mode 100644 index 000000000000..8fa6cf348018 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin_Test.h @@ -0,0 +1,17 @@ +// 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. + +// This header is available in the Test module. Import via "@import google_sign_in.Test;" + +#import + +@class GIDSignIn; + +/// Methods exposed for unit testing. +@interface FLTGoogleSignInPlugin () + +/// Inject @c GIDSignIn for testing. +- (instancetype)initWithSignIn:(GIDSignIn *)signIn NS_DESIGNATED_INITIALIZER; + +@end diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/google_sign_in-umbrella.h b/packages/google_sign_in/google_sign_in/ios/Classes/google_sign_in-umbrella.h new file mode 100644 index 000000000000..343c390f1782 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/ios/Classes/google_sign_in-umbrella.h @@ -0,0 +1,9 @@ +// 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 +#import + +FOUNDATION_EXPORT double google_sign_inVersionNumber; +FOUNDATION_EXPORT const unsigned char google_sign_inVersionString[]; diff --git a/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec b/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec index bf0b75f2957d..6b0741c65122 100644 --- a/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec +++ b/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec @@ -12,8 +12,9 @@ Enables Google Sign-In in Flutter apps. s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/google_sign_in' } - s.source_files = 'Classes/**/*' + s.source_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' + s.module_map = 'Classes/FLTGoogleSignInPlugin.modulemap' s.dependency 'Flutter' s.dependency 'GoogleSignIn', '~> 5.0' s.static_framework = true diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index a57f2197576d..14f7d8901301 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. repository: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 5.0.4 +version: 5.0.5 environment: sdk: ">=2.12.0 <3.0.0" From 036fb33ae18bd6a1d2af29302f2124132ca10768 Mon Sep 17 00:00:00 2001 From: Balvinder Singh Gambhir Date: Tue, 20 Jul 2021 06:36:03 +0530 Subject: [PATCH 012/123] [image_picker_for_web] Support multiple pick. Store name and other local file properties in XFile. (#4166) --- .../image_picker/image_picker_for_web/AUTHORS | 1 + .../image_picker_for_web/CHANGELOG.md | 6 + .../image_picker_for_web_test.dart | 81 +++++++++++-- .../lib/image_picker_for_web.dart | 113 ++++++++++++------ .../image_picker_for_web/pubspec.yaml | 2 +- 5 files changed, 159 insertions(+), 44 deletions(-) diff --git a/packages/image_picker/image_picker_for_web/AUTHORS b/packages/image_picker/image_picker_for_web/AUTHORS index 493a0b4ef9c2..d6ad42a677e5 100644 --- a/packages/image_picker/image_picker_for_web/AUTHORS +++ b/packages/image_picker/image_picker_for_web/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Balvinder Singh Gambhir diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index b0379ad2c07c..f32a5d8e92cd 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,3 +1,9 @@ +# 2.1.1 + +* Implemented `getMultiImage`. +* Initialized the following `XFile` attributes for picked files: + * `name`, `length`, `mimeType` and `lastModified`. + # 2.1.0 * Implemented `getImage`, `getVideo` and `getFile` methods that return `XFile` instances. diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart index c6d0b3b532ca..c1025a9f07d3 100644 --- a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart +++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart @@ -11,9 +11,16 @@ import 'package:image_picker_for_web/image_picker_for_web.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:integration_test/integration_test.dart'; -final String expectedStringContents = "Hello, world!"; +final String expectedStringContents = 'Hello, world!'; +final String otherStringContents = 'Hello again, world!'; final Uint8List bytes = utf8.encode(expectedStringContents) as Uint8List; -final html.File textFile = html.File([bytes], "hello.txt"); +final Uint8List otherBytes = utf8.encode(otherStringContents) as Uint8List; +final Map options = { + 'type': 'text/plain', + 'lastModified': DateTime.utc(2017, 12, 13).millisecondsSinceEpoch, +}; +final html.File textFile = html.File([bytes], 'hello.txt', options); +final html.File secondTextFile = html.File([otherBytes], 'secondFile.txt'); void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -30,7 +37,7 @@ void main() { final overrides = ImagePickerPluginTestOverrides() ..createInputElement = ((_, __) => mockInput) - ..getFileFromInput = ((_) => textFile); + ..getMultipleFilesFromInput = ((_) => [textFile]); final plugin = ImagePickerPlugin(overrides: overrides); @@ -51,20 +58,58 @@ void main() { final overrides = ImagePickerPluginTestOverrides() ..createInputElement = ((_, __) => mockInput) - ..getFileFromInput = ((_) => textFile); + ..getMultipleFilesFromInput = ((_) => [textFile]); final plugin = ImagePickerPlugin(overrides: overrides); // Init the pick file dialog... - final file = plugin.getFile(); + final image = plugin.getImage(source: ImageSource.camera); // Mock the browser behavior of selecting a file... mockInput.dispatchEvent(html.Event('change')); // Now the file should be available - expect(file, completes); + expect(image, completes); + // And readable - expect((await file).readAsBytes(), completion(isNotEmpty)); + final XFile file = await image; + expect(file.readAsBytes(), completion(isNotEmpty)); + expect(file.name, textFile.name); + expect(file.length(), completion(textFile.size)); + expect(file.mimeType, textFile.type); + expect( + file.lastModified(), + completion( + DateTime.fromMillisecondsSinceEpoch(textFile.lastModified!), + )); + }); + + testWidgets('Can select multiple files', (WidgetTester tester) async { + final mockInput = html.FileUploadInputElement(); + + final overrides = ImagePickerPluginTestOverrides() + ..createInputElement = ((_, __) => mockInput) + ..getMultipleFilesFromInput = ((_) => [textFile, secondTextFile]); + + final plugin = ImagePickerPlugin(overrides: overrides); + + // Init the pick file dialog... + final files = plugin.getMultiImage(); + + // Mock the browser behavior of selecting a file... + mockInput.dispatchEvent(html.Event('change')); + + // Now the file should be available + expect(files, completes); + + // And readable + expect((await files).first.readAsBytes(), completion(isNotEmpty)); + + // Peek into the second file... + final XFile secondFile = (await files).elementAt(1); + expect(secondFile.readAsBytes(), completion(isNotEmpty)); + expect(secondFile.name, secondTextFile.name); + expect(secondFile.length(), completion(secondTextFile.size)); }); // There's no good way of detecting when the user has "aborted" the selection. @@ -94,6 +139,7 @@ void main() { expect(input.attributes, containsPair('accept', 'any')); expect(input.attributes, isNot(contains('capture'))); + expect(input.attributes, isNot(contains('multiple'))); }); testWidgets('accept: any, capture: something', (WidgetTester tester) async { @@ -101,6 +147,27 @@ void main() { expect(input.attributes, containsPair('accept', 'any')); expect(input.attributes, containsPair('capture', 'something')); + expect(input.attributes, isNot(contains('multiple'))); + }); + + testWidgets('accept: any, capture: null, multi: true', + (WidgetTester tester) async { + html.Element input = + plugin.createInputElement('any', null, multiple: true); + + expect(input.attributes, containsPair('accept', 'any')); + expect(input.attributes, isNot(contains('capture'))); + expect(input.attributes, contains('multiple')); + }); + + testWidgets('accept: any, capture: something, multi: true', + (WidgetTester tester) async { + html.Element input = + plugin.createInputElement('any', 'something', multiple: true); + + expect(input.attributes, containsPair('accept', 'any')); + expect(input.attributes, containsPair('capture', 'something')); + expect(input.attributes, contains('multiple')); }); }); } diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index 08ce801cafbe..b170ee3256ab 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -18,6 +18,7 @@ final String _kAcceptVideoMimeType = 'video/3gpp,video/x-m4v,video/mp4,video/*'; /// This class implements the `package:image_picker` functionality for the web. class ImagePickerPlugin extends ImagePickerPlatform { final ImagePickerPluginTestOverrides? _overrides; + bool get _hasOverrides => _overrides != null; late html.Element _target; @@ -115,9 +116,13 @@ class ImagePickerPlugin extends ImagePickerPlatform { double? maxHeight, int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, - }) { + }) async { String? capture = computeCaptureAttribute(source, preferredCameraDevice); - return getFile(accept: _kAcceptImageMimeType, capture: capture); + List files = await getFiles( + accept: _kAcceptImageMimeType, + capture: capture, + ); + return files.first; } /// Returns an [XFile] containing the video that was picked. @@ -137,25 +142,48 @@ class ImagePickerPlugin extends ImagePickerPlatform { required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, Duration? maxDuration, - }) { + }) async { String? capture = computeCaptureAttribute(source, preferredCameraDevice); - return getFile(accept: _kAcceptVideoMimeType, capture: capture); + List files = await getFiles( + accept: _kAcceptVideoMimeType, + capture: capture, + ); + return files.first; + } + + /// Injects a file input, and returns a list of XFile that the user selected locally. + @override + Future> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) { + return getFiles(accept: _kAcceptImageMimeType, multiple: true); } /// Injects a file input with the specified accept+capture attributes, and - /// returns the PickedFile that the user selected locally. + /// returns a list of XFile that the user selected locally. /// /// `capture` is only supported in mobile browsers. + /// + /// `multiple` can be passed to allow for multiple selection of files. Defaults + /// to false. + /// /// See https://caniuse.com/#feat=html-media-capture @visibleForTesting - Future getFile({ + Future> getFiles({ String? accept, String? capture, + bool multiple = false, }) { - html.FileUploadInputElement input = - createInputElement(accept, capture) as html.FileUploadInputElement; + html.FileUploadInputElement input = createInputElement( + accept, + capture, + multiple: multiple, + ) as html.FileUploadInputElement; _injectAndActivate(input); - return _getSelectedXFile(input); + + return _getSelectedXFiles(input); } // DOM methods @@ -171,24 +199,19 @@ class ImagePickerPlugin extends ImagePickerPlatform { return null; } - html.File? _getFileFromInput(html.FileUploadInputElement input) { + List? _getFilesFromInput(html.FileUploadInputElement input) { if (_hasOverrides) { - return _overrides!.getFileFromInput(input); + return _overrides!.getMultipleFilesFromInput(input); } - return input.files?.first; + return input.files; } /// Handles the OnChange event from a FileUploadInputElement object - /// Returns the objectURL of the selected file. - String? _handleOnChangeEvent(html.Event event) { + /// Returns a list of selected files. + List? _handleOnChangeEvent(html.Event event) { final html.FileUploadInputElement input = event.target as html.FileUploadInputElement; - final html.File? file = _getFileFromInput(input); - - if (file != null) { - return html.Url.createObjectUrl(file); - } - return null; + return _getFilesFromInput(input); } /// Monitors an and returns the selected file. @@ -196,9 +219,11 @@ class ImagePickerPlugin extends ImagePickerPlatform { final Completer _completer = Completer(); // Observe the input until we can return something input.onChange.first.then((event) { - final objectUrl = _handleOnChangeEvent(event); - if (!_completer.isCompleted && objectUrl != null) { - _completer.complete(PickedFile(objectUrl)); + final files = _handleOnChangeEvent(event); + if (!_completer.isCompleted && files != null) { + _completer.complete(PickedFile( + html.Url.createObjectUrl(files.first), + )); } }); input.onError.first.then((event) { @@ -212,13 +237,24 @@ class ImagePickerPlugin extends ImagePickerPlatform { return _completer.future; } - Future _getSelectedXFile(html.FileUploadInputElement input) { - final Completer _completer = Completer(); + /// Monitors an and returns the selected file(s). + Future> _getSelectedXFiles(html.FileUploadInputElement input) { + final Completer> _completer = Completer>(); // Observe the input until we can return something input.onChange.first.then((event) { - final objectUrl = _handleOnChangeEvent(event); - if (!_completer.isCompleted && objectUrl != null) { - _completer.complete(XFile(objectUrl)); + final files = _handleOnChangeEvent(event); + if (!_completer.isCompleted && files != null) { + _completer.complete(files + .map((file) => XFile( + html.Url.createObjectUrl(file), + name: file.name, + length: file.size, + lastModified: DateTime.fromMillisecondsSinceEpoch( + file.lastModified ?? DateTime.now().millisecondsSinceEpoch, + ), + mimeType: file.type, + )) + .toList()); } }); input.onError.first.then((event) { @@ -248,12 +284,18 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// Creates an input element that accepts certain file types, and /// allows to `capture` from the device's cameras (where supported) @visibleForTesting - html.Element createInputElement(String? accept, String? capture) { + html.Element createInputElement( + String? accept, + String? capture, { + bool multiple = false, + }) { if (_hasOverrides) { return _overrides!.createInputElement(accept, capture); } - html.Element element = html.FileUploadInputElement()..accept = accept; + html.Element element = html.FileUploadInputElement() + ..accept = accept + ..multiple = multiple; if (capture != null) { element.setAttribute('capture', capture); @@ -278,11 +320,10 @@ typedef OverrideCreateInputFunction = html.Element Function( String? capture, ); -/// A function that extracts a [html.File] from the file `input` passed in. +/// A function that extracts list of files from the file `input` passed in. @visibleForTesting -typedef OverrideExtractFilesFromInputFunction = html.File Function( - html.Element? input, -); +typedef OverrideExtractMultipleFilesFromInputFunction = List + Function(html.Element? input); /// Overrides for some of the functionality above. @visibleForTesting @@ -290,6 +331,6 @@ class ImagePickerPluginTestOverrides { /// Override the creation of the input element. late OverrideCreateInputFunction createInputElement; - /// Override the extraction of the selected file from an input element. - late OverrideExtractFilesFromInputFunction getFileFromInput; + /// Override the extraction of the selected files from an input element. + late OverrideExtractMultipleFilesFromInputFunction getMultipleFilesFromInput; } diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index d9b9c5e5cb86..b2479285a3ea 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_for_web description: Web platform implementation of image_picker repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 2.1.0 +version: 2.1.1 environment: sdk: ">=2.12.0 <3.0.0" From a807b5c7b1f70e6e75833b50deb384a2b736ac14 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 20 Jul 2021 10:01:07 -0700 Subject: [PATCH 013/123] [flutter_plugin_tools] Use -version with java (#4171) --- script/tool/CHANGELOG.md | 4 +++- script/tool/lib/src/format_command.dart | 5 ++++- script/tool/pubspec.yaml | 2 +- script/tool/test/format_command_test.dart | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 17b28927538d..1e447721d13f 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,6 +1,8 @@ -## NEXT +## 0.4.1 - Improved `license-check` output. +- Use `java -version` rather than `java --version`, for compatibility with more + versions of Java. ## 0.4.0 diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index c67fb96d2835..d09a94b1aefe 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -244,9 +244,12 @@ class FormatCommand extends PluginCommand { /// Returns true if [command] can be run successfully. Future _hasDependency(String command) async { + // Some versions of Java accept both -version and --version, but some only + // accept -version. + final String versionFlag = command == 'java' ? '-version' : '--version'; try { final io.ProcessResult result = - await processRunner.run(command, ['--version']); + await processRunner.run(command, [versionFlag]); if (result.exitCode != 0) { return false; } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 7dadc598d4b4..7b2cdd4f4101 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.4.0 +version: 0.4.1 dependencies: args: ^2.1.0 diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index 4728c3136556..b072e5d30aaf 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -146,7 +146,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - const ProcessCall('java', ['--version'], null), + const ProcessCall('java', ['-version'], null), ProcessCall( 'java', [ From 0bbef40ceeedca87882c27b0c33215d92f737ee2 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 20 Jul 2021 19:31:05 +0200 Subject: [PATCH 014/123] [webview_flutter] Move webview_flutter to webview_flutter/webview_flutter (#4152) --- .../webview_flutter/{ => webview_flutter}/AUTHORS | 0 .../{ => webview_flutter}/CHANGELOG.md | 0 .../webview_flutter/{ => webview_flutter}/LICENSE | 0 .../webview_flutter/{ => webview_flutter}/README.md | 0 .../{ => webview_flutter}/android/build.gradle | 0 .../{ => webview_flutter}/android/settings.gradle | 0 .../android/src/main/AndroidManifest.xml | 0 .../webviewflutter/DisplayListenerProxy.java | 0 .../webviewflutter/FlutterCookieManager.java | 0 .../plugins/webviewflutter/FlutterWebView.java | 0 .../webviewflutter/FlutterWebViewClient.java | 0 .../plugins/webviewflutter/InputAwareWebView.java | 0 .../plugins/webviewflutter/JavaScriptChannel.java | 0 .../ThreadedInputConnectionProxyAdapterView.java | 0 .../plugins/webviewflutter/WebViewFactory.java | 0 .../webviewflutter/WebViewFlutterPlugin.java | 0 .../flutter/plugins/webviewflutter/WebViewTest.java | 0 .../{ => webview_flutter}/example/.metadata | 0 .../{ => webview_flutter}/example/README.md | 0 .../example/android/app/build.gradle | 0 .../app/gradle/wrapper/gradle-wrapper.properties | 0 .../EmbeddingV1ActivityTest.java | 0 .../webviewflutterexample/MainActivityTest.java | 0 .../android/app/src/main/AndroidManifest.xml | 0 .../app/src/main/res/drawable/launch_background.xml | 0 .../app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../app/src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../app/src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../android/app/src/main/res/values/styles.xml | 0 .../example/android/build.gradle | 0 .../example/android/gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 .../example/android/settings.gradle | 0 .../example/assets/sample_audio.ogg | Bin .../example/assets/sample_video.mp4 | Bin .../integration_test/webview_flutter_test.dart | 0 .../example/ios/Flutter/AppFrameworkInfo.plist | 0 .../example/ios/Flutter/Debug.xcconfig | 0 .../example/ios/Flutter/Release.xcconfig | 0 .../{ => webview_flutter}/example/ios/Podfile | 0 .../example/ios/Runner.xcodeproj/project.pbxproj | 0 .../project.xcworkspace/contents.xcworkspacedata | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../ios/Runner.xcworkspace/contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../example/ios/Runner/AppDelegate.h | 0 .../example/ios/Runner/AppDelegate.m | 0 .../AppIcon.appiconset/Contents.json | 0 .../AppIcon.appiconset/Icon-App-1024x1024@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin .../AppIcon.appiconset/Icon-App-83.5x83.5@2x.png | Bin .../LaunchImage.imageset/Contents.json | 0 .../LaunchImage.imageset/LaunchImage.png | Bin .../LaunchImage.imageset/LaunchImage@2x.png | Bin .../LaunchImage.imageset/LaunchImage@3x.png | Bin .../Assets.xcassets/LaunchImage.imageset/README.md | 0 .../ios/Runner/Base.lproj/LaunchScreen.storyboard | 0 .../example/ios/Runner/Base.lproj/Main.storyboard | 0 .../example/ios/Runner/Info.plist | 0 .../{ => webview_flutter}/example/ios/Runner/main.m | 0 .../ios/RunnerTests/FLTWKNavigationDelegateTests.m | 0 .../example/ios/RunnerTests/FLTWebViewTests.m | 0 .../example/ios/RunnerTests/Info.plist | 0 .../example/ios/RunnerUITests/FLTWebViewUITests.m | 0 .../example/ios/RunnerUITests/Info.plist | 0 .../{ => webview_flutter}/example/lib/main.dart | 0 .../{ => webview_flutter}/example/pubspec.yaml | 0 .../example/test_driver/integration_test.dart | 0 .../{ => webview_flutter}/ios/Assets/.gitkeep | 0 .../ios/Classes/FLTCookieManager.h | 0 .../ios/Classes/FLTCookieManager.m | 0 .../ios/Classes/FLTWKNavigationDelegate.h | 0 .../ios/Classes/FLTWKNavigationDelegate.m | 0 .../ios/Classes/FLTWKProgressionDelegate.h | 0 .../ios/Classes/FLTWKProgressionDelegate.m | 0 .../ios/Classes/FLTWebViewFlutterPlugin.h | 0 .../ios/Classes/FLTWebViewFlutterPlugin.m | 0 .../ios/Classes/FlutterWebView.h | 0 .../ios/Classes/FlutterWebView.m | 0 .../ios/Classes/JavaScriptChannelHandler.h | 0 .../ios/Classes/JavaScriptChannelHandler.m | 0 .../ios/webview_flutter.podspec | 0 .../lib/platform_interface.dart | 0 .../lib/src/webview_android.dart | 0 .../lib/src/webview_cupertino.dart | 0 .../lib/src/webview_method_channel.dart | 0 .../{ => webview_flutter}/lib/webview_flutter.dart | 0 .../{ => webview_flutter}/pubspec.yaml | 2 +- .../test/webview_flutter_test.dart | 0 103 files changed, 1 insertion(+), 1 deletion(-) rename packages/webview_flutter/{ => webview_flutter}/AUTHORS (100%) rename packages/webview_flutter/{ => webview_flutter}/CHANGELOG.md (100%) rename packages/webview_flutter/{ => webview_flutter}/LICENSE (100%) rename packages/webview_flutter/{ => webview_flutter}/README.md (100%) rename packages/webview_flutter/{ => webview_flutter}/android/build.gradle (100%) rename packages/webview_flutter/{ => webview_flutter}/android/settings.gradle (100%) rename packages/webview_flutter/{ => webview_flutter}/android/src/main/AndroidManifest.xml (100%) rename packages/webview_flutter/{ => webview_flutter}/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java (100%) rename packages/webview_flutter/{ => webview_flutter}/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java (100%) rename packages/webview_flutter/{ => webview_flutter}/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java (100%) rename packages/webview_flutter/{ => webview_flutter}/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java (100%) rename packages/webview_flutter/{ => webview_flutter}/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java (100%) rename packages/webview_flutter/{ => webview_flutter}/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java (100%) rename packages/webview_flutter/{ => webview_flutter}/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java (100%) rename packages/webview_flutter/{ => webview_flutter}/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java (100%) rename packages/webview_flutter/{ => webview_flutter}/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java (100%) rename packages/webview_flutter/{ => webview_flutter}/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java (100%) rename packages/webview_flutter/{ => webview_flutter}/example/.metadata (100%) rename packages/webview_flutter/{ => webview_flutter}/example/README.md (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/app/build.gradle (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/app/gradle/wrapper/gradle-wrapper.properties (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/app/src/main/AndroidManifest.xml (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/app/src/main/res/drawable/launch_background.xml (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/app/src/main/res/values/styles.xml (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/build.gradle (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/gradle.properties (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/gradle/wrapper/gradle-wrapper.properties (100%) rename packages/webview_flutter/{ => webview_flutter}/example/android/settings.gradle (100%) rename packages/webview_flutter/{ => webview_flutter}/example/assets/sample_audio.ogg (100%) rename packages/webview_flutter/{ => webview_flutter}/example/assets/sample_video.mp4 (100%) rename packages/webview_flutter/{ => webview_flutter}/example/integration_test/webview_flutter_test.dart (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Flutter/AppFrameworkInfo.plist (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Flutter/Debug.xcconfig (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Flutter/Release.xcconfig (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Podfile (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner.xcodeproj/project.pbxproj (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner.xcworkspace/contents.xcworkspacedata (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/AppDelegate.h (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/AppDelegate.m (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Base.lproj/LaunchScreen.storyboard (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Base.lproj/Main.storyboard (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/Info.plist (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/Runner/main.m (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/RunnerTests/FLTWebViewTests.m (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/RunnerTests/Info.plist (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/RunnerUITests/FLTWebViewUITests.m (100%) rename packages/webview_flutter/{ => webview_flutter}/example/ios/RunnerUITests/Info.plist (100%) rename packages/webview_flutter/{ => webview_flutter}/example/lib/main.dart (100%) rename packages/webview_flutter/{ => webview_flutter}/example/pubspec.yaml (100%) rename packages/webview_flutter/{ => webview_flutter}/example/test_driver/integration_test.dart (100%) rename packages/webview_flutter/{ => webview_flutter}/ios/Assets/.gitkeep (100%) rename packages/webview_flutter/{ => webview_flutter}/ios/Classes/FLTCookieManager.h (100%) rename packages/webview_flutter/{ => webview_flutter}/ios/Classes/FLTCookieManager.m (100%) rename packages/webview_flutter/{ => webview_flutter}/ios/Classes/FLTWKNavigationDelegate.h (100%) rename packages/webview_flutter/{ => webview_flutter}/ios/Classes/FLTWKNavigationDelegate.m (100%) rename packages/webview_flutter/{ => webview_flutter}/ios/Classes/FLTWKProgressionDelegate.h (100%) rename packages/webview_flutter/{ => webview_flutter}/ios/Classes/FLTWKProgressionDelegate.m (100%) rename packages/webview_flutter/{ => webview_flutter}/ios/Classes/FLTWebViewFlutterPlugin.h (100%) rename packages/webview_flutter/{ => webview_flutter}/ios/Classes/FLTWebViewFlutterPlugin.m (100%) rename packages/webview_flutter/{ => webview_flutter}/ios/Classes/FlutterWebView.h (100%) rename packages/webview_flutter/{ => webview_flutter}/ios/Classes/FlutterWebView.m (100%) rename packages/webview_flutter/{ => webview_flutter}/ios/Classes/JavaScriptChannelHandler.h (100%) rename packages/webview_flutter/{ => webview_flutter}/ios/Classes/JavaScriptChannelHandler.m (100%) rename packages/webview_flutter/{ => webview_flutter}/ios/webview_flutter.podspec (100%) rename packages/webview_flutter/{ => webview_flutter}/lib/platform_interface.dart (100%) rename packages/webview_flutter/{ => webview_flutter}/lib/src/webview_android.dart (100%) rename packages/webview_flutter/{ => webview_flutter}/lib/src/webview_cupertino.dart (100%) rename packages/webview_flutter/{ => webview_flutter}/lib/src/webview_method_channel.dart (100%) rename packages/webview_flutter/{ => webview_flutter}/lib/webview_flutter.dart (100%) rename packages/webview_flutter/{ => webview_flutter}/pubspec.yaml (95%) rename packages/webview_flutter/{ => webview_flutter}/test/webview_flutter_test.dart (100%) diff --git a/packages/webview_flutter/AUTHORS b/packages/webview_flutter/webview_flutter/AUTHORS similarity index 100% rename from packages/webview_flutter/AUTHORS rename to packages/webview_flutter/webview_flutter/AUTHORS diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md similarity index 100% rename from packages/webview_flutter/CHANGELOG.md rename to packages/webview_flutter/webview_flutter/CHANGELOG.md diff --git a/packages/webview_flutter/LICENSE b/packages/webview_flutter/webview_flutter/LICENSE similarity index 100% rename from packages/webview_flutter/LICENSE rename to packages/webview_flutter/webview_flutter/LICENSE diff --git a/packages/webview_flutter/README.md b/packages/webview_flutter/webview_flutter/README.md similarity index 100% rename from packages/webview_flutter/README.md rename to packages/webview_flutter/webview_flutter/README.md diff --git a/packages/webview_flutter/android/build.gradle b/packages/webview_flutter/webview_flutter/android/build.gradle similarity index 100% rename from packages/webview_flutter/android/build.gradle rename to packages/webview_flutter/webview_flutter/android/build.gradle diff --git a/packages/webview_flutter/android/settings.gradle b/packages/webview_flutter/webview_flutter/android/settings.gradle similarity index 100% rename from packages/webview_flutter/android/settings.gradle rename to packages/webview_flutter/webview_flutter/android/settings.gradle diff --git a/packages/webview_flutter/android/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/webview_flutter/android/src/main/AndroidManifest.xml rename to packages/webview_flutter/webview_flutter/android/src/main/AndroidManifest.xml diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java similarity index 100% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java rename to packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java similarity index 100% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java rename to packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java similarity index 100% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java rename to packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java similarity index 100% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java rename to packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java similarity index 100% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java rename to packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java similarity index 100% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java rename to packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java similarity index 100% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java rename to packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java similarity index 100% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java rename to packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java similarity index 100% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java rename to packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java diff --git a/packages/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java similarity index 100% rename from packages/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java rename to packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java diff --git a/packages/webview_flutter/example/.metadata b/packages/webview_flutter/webview_flutter/example/.metadata similarity index 100% rename from packages/webview_flutter/example/.metadata rename to packages/webview_flutter/webview_flutter/example/.metadata diff --git a/packages/webview_flutter/example/README.md b/packages/webview_flutter/webview_flutter/example/README.md similarity index 100% rename from packages/webview_flutter/example/README.md rename to packages/webview_flutter/webview_flutter/example/README.md diff --git a/packages/webview_flutter/example/android/app/build.gradle b/packages/webview_flutter/webview_flutter/example/android/app/build.gradle similarity index 100% rename from packages/webview_flutter/example/android/app/build.gradle rename to packages/webview_flutter/webview_flutter/example/android/app/build.gradle diff --git a/packages/webview_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/webview_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/webview_flutter/webview_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java similarity index 100% rename from packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java rename to packages/webview_flutter/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java diff --git a/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java similarity index 100% rename from packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java rename to packages/webview_flutter/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java diff --git a/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml rename to packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml diff --git a/packages/webview_flutter/example/android/app/src/main/res/drawable/launch_background.xml b/packages/webview_flutter/webview_flutter/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from packages/webview_flutter/example/android/app/src/main/res/drawable/launch_background.xml rename to packages/webview_flutter/webview_flutter/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/packages/webview_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/webview_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/webview_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/webview_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/webview_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/webview_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/webview_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/webview_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/webview_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/webview_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/webview_flutter/example/android/app/src/main/res/values/styles.xml b/packages/webview_flutter/webview_flutter/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from packages/webview_flutter/example/android/app/src/main/res/values/styles.xml rename to packages/webview_flutter/webview_flutter/example/android/app/src/main/res/values/styles.xml diff --git a/packages/webview_flutter/example/android/build.gradle b/packages/webview_flutter/webview_flutter/example/android/build.gradle similarity index 100% rename from packages/webview_flutter/example/android/build.gradle rename to packages/webview_flutter/webview_flutter/example/android/build.gradle diff --git a/packages/webview_flutter/example/android/gradle.properties b/packages/webview_flutter/webview_flutter/example/android/gradle.properties similarity index 100% rename from packages/webview_flutter/example/android/gradle.properties rename to packages/webview_flutter/webview_flutter/example/android/gradle.properties diff --git a/packages/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/webview_flutter/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/webview_flutter/example/android/settings.gradle b/packages/webview_flutter/webview_flutter/example/android/settings.gradle similarity index 100% rename from packages/webview_flutter/example/android/settings.gradle rename to packages/webview_flutter/webview_flutter/example/android/settings.gradle diff --git a/packages/webview_flutter/example/assets/sample_audio.ogg b/packages/webview_flutter/webview_flutter/example/assets/sample_audio.ogg similarity index 100% rename from packages/webview_flutter/example/assets/sample_audio.ogg rename to packages/webview_flutter/webview_flutter/example/assets/sample_audio.ogg diff --git a/packages/webview_flutter/example/assets/sample_video.mp4 b/packages/webview_flutter/webview_flutter/example/assets/sample_video.mp4 similarity index 100% rename from packages/webview_flutter/example/assets/sample_video.mp4 rename to packages/webview_flutter/webview_flutter/example/assets/sample_video.mp4 diff --git a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart similarity index 100% rename from packages/webview_flutter/example/integration_test/webview_flutter_test.dart rename to packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart diff --git a/packages/webview_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/packages/webview_flutter/webview_flutter/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from packages/webview_flutter/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/webview_flutter/webview_flutter/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/packages/webview_flutter/example/ios/Flutter/Debug.xcconfig b/packages/webview_flutter/webview_flutter/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/webview_flutter/example/ios/Flutter/Debug.xcconfig rename to packages/webview_flutter/webview_flutter/example/ios/Flutter/Debug.xcconfig diff --git a/packages/webview_flutter/example/ios/Flutter/Release.xcconfig b/packages/webview_flutter/webview_flutter/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/webview_flutter/example/ios/Flutter/Release.xcconfig rename to packages/webview_flutter/webview_flutter/example/ios/Flutter/Release.xcconfig diff --git a/packages/webview_flutter/example/ios/Podfile b/packages/webview_flutter/webview_flutter/example/ios/Podfile similarity index 100% rename from packages/webview_flutter/example/ios/Podfile rename to packages/webview_flutter/webview_flutter/example/ios/Podfile diff --git a/packages/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from packages/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj rename to packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj diff --git a/packages/webview_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/webview_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/packages/webview_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from packages/webview_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/packages/webview_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/webview_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/webview_flutter/webview_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/webview_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/webview_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/webview_flutter/webview_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/webview_flutter/example/ios/Runner/AppDelegate.h b/packages/webview_flutter/webview_flutter/example/ios/Runner/AppDelegate.h similarity index 100% rename from packages/webview_flutter/example/ios/Runner/AppDelegate.h rename to packages/webview_flutter/webview_flutter/example/ios/Runner/AppDelegate.h diff --git a/packages/webview_flutter/example/ios/Runner/AppDelegate.m b/packages/webview_flutter/webview_flutter/example/ios/Runner/AppDelegate.m similarity index 100% rename from packages/webview_flutter/example/ios/Runner/AppDelegate.m rename to packages/webview_flutter/webview_flutter/example/ios/Runner/AppDelegate.m diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/packages/webview_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/webview_flutter/webview_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/webview_flutter/example/ios/Runner/Base.lproj/Main.storyboard b/packages/webview_flutter/webview_flutter/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/webview_flutter/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Info.plist rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist diff --git a/packages/webview_flutter/example/ios/Runner/main.m b/packages/webview_flutter/webview_flutter/example/ios/Runner/main.m similarity index 100% rename from packages/webview_flutter/example/ios/Runner/main.m rename to packages/webview_flutter/webview_flutter/example/ios/Runner/main.m diff --git a/packages/webview_flutter/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m b/packages/webview_flutter/webview_flutter/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m similarity index 100% rename from packages/webview_flutter/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m rename to packages/webview_flutter/webview_flutter/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m diff --git a/packages/webview_flutter/example/ios/RunnerTests/FLTWebViewTests.m b/packages/webview_flutter/webview_flutter/example/ios/RunnerTests/FLTWebViewTests.m similarity index 100% rename from packages/webview_flutter/example/ios/RunnerTests/FLTWebViewTests.m rename to packages/webview_flutter/webview_flutter/example/ios/RunnerTests/FLTWebViewTests.m diff --git a/packages/webview_flutter/example/ios/RunnerTests/Info.plist b/packages/webview_flutter/webview_flutter/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/webview_flutter/example/ios/RunnerTests/Info.plist rename to packages/webview_flutter/webview_flutter/example/ios/RunnerTests/Info.plist diff --git a/packages/webview_flutter/example/ios/RunnerUITests/FLTWebViewUITests.m b/packages/webview_flutter/webview_flutter/example/ios/RunnerUITests/FLTWebViewUITests.m similarity index 100% rename from packages/webview_flutter/example/ios/RunnerUITests/FLTWebViewUITests.m rename to packages/webview_flutter/webview_flutter/example/ios/RunnerUITests/FLTWebViewUITests.m diff --git a/packages/webview_flutter/example/ios/RunnerUITests/Info.plist b/packages/webview_flutter/webview_flutter/example/ios/RunnerUITests/Info.plist similarity index 100% rename from packages/webview_flutter/example/ios/RunnerUITests/Info.plist rename to packages/webview_flutter/webview_flutter/example/ios/RunnerUITests/Info.plist diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart similarity index 100% rename from packages/webview_flutter/example/lib/main.dart rename to packages/webview_flutter/webview_flutter/example/lib/main.dart diff --git a/packages/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/webview_flutter/example/pubspec.yaml similarity index 100% rename from packages/webview_flutter/example/pubspec.yaml rename to packages/webview_flutter/webview_flutter/example/pubspec.yaml diff --git a/packages/webview_flutter/example/test_driver/integration_test.dart b/packages/webview_flutter/webview_flutter/example/test_driver/integration_test.dart similarity index 100% rename from packages/webview_flutter/example/test_driver/integration_test.dart rename to packages/webview_flutter/webview_flutter/example/test_driver/integration_test.dart diff --git a/packages/webview_flutter/ios/Assets/.gitkeep b/packages/webview_flutter/webview_flutter/ios/Assets/.gitkeep similarity index 100% rename from packages/webview_flutter/ios/Assets/.gitkeep rename to packages/webview_flutter/webview_flutter/ios/Assets/.gitkeep diff --git a/packages/webview_flutter/ios/Classes/FLTCookieManager.h b/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.h similarity index 100% rename from packages/webview_flutter/ios/Classes/FLTCookieManager.h rename to packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.h diff --git a/packages/webview_flutter/ios/Classes/FLTCookieManager.m b/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.m similarity index 100% rename from packages/webview_flutter/ios/Classes/FLTCookieManager.m rename to packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.m diff --git a/packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.h b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.h similarity index 100% rename from packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.h rename to packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.h diff --git a/packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m similarity index 100% rename from packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m rename to packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m diff --git a/packages/webview_flutter/ios/Classes/FLTWKProgressionDelegate.h b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKProgressionDelegate.h similarity index 100% rename from packages/webview_flutter/ios/Classes/FLTWKProgressionDelegate.h rename to packages/webview_flutter/webview_flutter/ios/Classes/FLTWKProgressionDelegate.h diff --git a/packages/webview_flutter/ios/Classes/FLTWKProgressionDelegate.m b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKProgressionDelegate.m similarity index 100% rename from packages/webview_flutter/ios/Classes/FLTWKProgressionDelegate.m rename to packages/webview_flutter/webview_flutter/ios/Classes/FLTWKProgressionDelegate.m diff --git a/packages/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.h b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.h similarity index 100% rename from packages/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.h rename to packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.h diff --git a/packages/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.m b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.m similarity index 100% rename from packages/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.m rename to packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.m diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.h b/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.h similarity index 100% rename from packages/webview_flutter/ios/Classes/FlutterWebView.h rename to packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.h diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.m similarity index 100% rename from packages/webview_flutter/ios/Classes/FlutterWebView.m rename to packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.m diff --git a/packages/webview_flutter/ios/Classes/JavaScriptChannelHandler.h b/packages/webview_flutter/webview_flutter/ios/Classes/JavaScriptChannelHandler.h similarity index 100% rename from packages/webview_flutter/ios/Classes/JavaScriptChannelHandler.h rename to packages/webview_flutter/webview_flutter/ios/Classes/JavaScriptChannelHandler.h diff --git a/packages/webview_flutter/ios/Classes/JavaScriptChannelHandler.m b/packages/webview_flutter/webview_flutter/ios/Classes/JavaScriptChannelHandler.m similarity index 100% rename from packages/webview_flutter/ios/Classes/JavaScriptChannelHandler.m rename to packages/webview_flutter/webview_flutter/ios/Classes/JavaScriptChannelHandler.m diff --git a/packages/webview_flutter/ios/webview_flutter.podspec b/packages/webview_flutter/webview_flutter/ios/webview_flutter.podspec similarity index 100% rename from packages/webview_flutter/ios/webview_flutter.podspec rename to packages/webview_flutter/webview_flutter/ios/webview_flutter.podspec diff --git a/packages/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/webview_flutter/lib/platform_interface.dart similarity index 100% rename from packages/webview_flutter/lib/platform_interface.dart rename to packages/webview_flutter/webview_flutter/lib/platform_interface.dart diff --git a/packages/webview_flutter/lib/src/webview_android.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_android.dart similarity index 100% rename from packages/webview_flutter/lib/src/webview_android.dart rename to packages/webview_flutter/webview_flutter/lib/src/webview_android.dart diff --git a/packages/webview_flutter/lib/src/webview_cupertino.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_cupertino.dart similarity index 100% rename from packages/webview_flutter/lib/src/webview_cupertino.dart rename to packages/webview_flutter/webview_flutter/lib/src/webview_cupertino.dart diff --git a/packages/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_method_channel.dart similarity index 100% rename from packages/webview_flutter/lib/src/webview_method_channel.dart rename to packages/webview_flutter/webview_flutter/lib/src/webview_method_channel.dart diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart similarity index 100% rename from packages/webview_flutter/lib/webview_flutter.dart rename to packages/webview_flutter/webview_flutter/lib/webview_flutter.dart diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml similarity index 95% rename from packages/webview_flutter/pubspec.yaml rename to packages/webview_flutter/webview_flutter/pubspec.yaml index 4d984beeed96..88ab4ad7927e 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. -repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter +repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 version: 2.0.10 diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart similarity index 100% rename from packages/webview_flutter/test/webview_flutter_test.dart rename to packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart From 337dc68d7b67eee25ac6ca7ebf071751a9d760c9 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Tue, 20 Jul 2021 21:30:50 +0200 Subject: [PATCH 015/123] [camera_web] Add Camera class to control video and take pictures (#4168) --- .../example/integration_test/camera_test.dart | 487 ++++++++++++++++++ .../camera/camera_web/lib/src/camera.dart | 196 +++++++ .../camera_web/lib/src/shims/dart_ui.dart | 10 + .../lib/src/shims/dart_ui_fake.dart | 28 + .../lib/src/shims/dart_ui_real.dart | 5 + .../lib/src/types/camera_error_codes.dart | 29 ++ .../camera_web/lib/src/types/types.dart | 1 + 7 files changed, 756 insertions(+) create mode 100644 packages/camera/camera_web/example/integration_test/camera_test.dart create mode 100644 packages/camera/camera_web/lib/src/camera.dart create mode 100644 packages/camera/camera_web/lib/src/shims/dart_ui.dart create mode 100644 packages/camera/camera_web/lib/src/shims/dart_ui_fake.dart create mode 100644 packages/camera/camera_web/lib/src/shims/dart_ui_real.dart create mode 100644 packages/camera/camera_web/lib/src/types/camera_error_codes.dart diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart new file mode 100644 index 000000000000..0f1dcf7049d9 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -0,0 +1,487 @@ +// 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 'dart:html'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/types/camera_error_codes.dart'; +import 'package:camera_web/src/types/camera_options.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Camera', () { + late Window window; + late Navigator navigator; + late MediaStream mediaStream; + late MediaDevices mediaDevices; + + setUp(() { + window = MockWindow(); + navigator = MockNavigator(); + mediaDevices = MockMediaDevices(); + + final videoElement = VideoElement() + ..src = + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4' + ..preload = 'true' + ..width = 10 + ..height = 10; + + mediaStream = videoElement.captureStream(); + + when(() => window.navigator).thenReturn(navigator); + when(() => navigator.mediaDevices).thenReturn(mediaDevices); + when( + () => mediaDevices.getUserMedia(any()), + ).thenAnswer((_) async => mediaStream); + }); + + group('initialize', () { + testWidgets( + 'creates a video element ' + 'with correct properties', (tester) async { + const audioConstraints = AudioConstraints(enabled: true); + + final camera = Camera( + textureId: 1, + options: CameraOptions( + audio: audioConstraints, + ), + window: window, + ); + + await camera.initialize(); + + expect(camera.videoElement, isNotNull); + expect(camera.videoElement.autoplay, isFalse); + expect(camera.videoElement.muted, !audioConstraints.enabled); + expect(camera.videoElement.srcObject, mediaStream); + expect(camera.videoElement.attributes.keys, contains('playsinline')); + + expect( + camera.videoElement.style.transformOrigin, equals('center center')); + expect(camera.videoElement.style.pointerEvents, equals('none')); + expect(camera.videoElement.style.width, equals('100%')); + expect(camera.videoElement.style.height, equals('100%')); + expect(camera.videoElement.style.objectFit, equals('cover')); + expect(camera.videoElement.style.transform, equals('scaleX(-1)')); + }); + + testWidgets( + 'creates a wrapping div element ' + 'with correct properties', (tester) async { + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + + expect(camera.divElement, isNotNull); + expect(camera.divElement.style.objectFit, equals('cover')); + expect(camera.divElement.children, contains(camera.videoElement)); + }); + + testWidgets('calls getUserMedia with provided options', (tester) async { + final options = CameraOptions( + video: VideoConstraints( + facingMode: FacingModeConstraint.exact(CameraType.user), + width: VideoSizeConstraint(ideal: 200), + ), + ); + + final optionsJson = await options.toJson(); + + final camera = Camera( + textureId: 1, + options: options, + window: window, + ); + + await camera.initialize(); + + verify(() => mediaDevices.getUserMedia(optionsJson)).called(1); + }); + + group('throws CameraException', () { + testWidgets( + 'with notSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notSupported, + ), + ), + ); + }); + + testWidgets( + 'with notFound error ' + 'when getUserMedia throws DomException ' + 'with NotFoundError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('NotFoundError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notFound, + ), + ), + ); + }); + + testWidgets( + 'with notFound error ' + 'when getUserMedia throws DomException ' + 'with DevicesNotFoundError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('DevicesNotFoundError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notFound, + ), + ), + ); + }); + + testWidgets( + 'with notReadable error ' + 'when getUserMedia throws DomException ' + 'with NotReadableError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('NotReadableError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notReadable, + ), + ), + ); + }); + + testWidgets( + 'with notReadable error ' + 'when getUserMedia throws DomException ' + 'with TrackStartError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('TrackStartError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notReadable, + ), + ), + ); + }); + + testWidgets( + 'with overconstrained error ' + 'when getUserMedia throws DomException ' + 'with OverconstrainedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('OverconstrainedError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.overconstrained, + ), + ), + ); + }); + + testWidgets( + 'with overconstrained error ' + 'when getUserMedia throws DomException ' + 'with ConstraintNotSatisfiedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('ConstraintNotSatisfiedError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.overconstrained, + ), + ), + ); + }); + + testWidgets( + 'with permissionDenied error ' + 'when getUserMedia throws DomException ' + 'with NotAllowedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('NotAllowedError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.permissionDenied, + ), + ), + ); + }); + + testWidgets( + 'with permissionDenied error ' + 'when getUserMedia throws DomException ' + 'with PermissionDeniedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('PermissionDeniedError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.permissionDenied, + ), + ), + ); + }); + + testWidgets( + 'with type error ' + 'when getUserMedia throws DomException ' + 'with TypeError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('TypeError')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.type, + ), + ), + ); + }); + + testWidgets( + 'with unknown error ' + 'when getUserMedia throws DomException ' + 'with an unknown error', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('Unknown')); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.unknown, + ), + ), + ); + }); + + testWidgets( + 'with unknown error ' + 'when getUserMedia throws an unknown exception', (tester) async { + when(() => mediaDevices.getUserMedia(any())).thenThrow(Exception()); + + final camera = Camera( + textureId: 1, + window: window, + ); + + expect( + camera.initialize, + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.unknown, + ), + ), + ); + }); + }); + }); + + group('play', () { + testWidgets('starts playing the video element', (tester) async { + var startedPlaying = false; + + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + + camera.videoElement.onPlay.listen((event) => startedPlaying = true); + + await camera.play(); + + expect(startedPlaying, isTrue); + }); + + testWidgets( + 'assigns media stream to the video element\'s source ' + 'if it does not exist', (tester) async { + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + + /// Remove the video element's source + /// by stopping the camera. + // ignore: cascade_invocations + camera.stop(); + + await camera.play(); + + expect(camera.videoElement.srcObject, mediaStream); + }); + }); + + group('stop', () { + testWidgets('resets the video element\'s source', (tester) async { + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + await camera.play(); + + camera.stop(); + + expect(camera.videoElement.srcObject, isNull); + }); + }); + + group('takePicture', () { + testWidgets('returns a captured picture', (tester) async { + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + await camera.play(); + + final pictureFile = await camera.takePicture(); + + expect(pictureFile, isNotNull); + }); + }); + + group('dispose', () { + testWidgets('resets the video element\'s source', (tester) async { + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + + camera.dispose(); + + expect(camera.videoElement.srcObject, isNull); + }); + }); + }); +} diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart new file mode 100644 index 000000000000..41692d548882 --- /dev/null +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -0,0 +1,196 @@ +// 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 'dart:html' as html; +import 'shims/dart_ui.dart' as ui; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/types/camera_error_codes.dart'; +import 'package:camera_web/src/types/camera_options.dart'; + +String _getViewType(int cameraId) => 'plugins.flutter.io/camera_$cameraId'; + +/// A camera initialized from the media devices in the current [window]. +/// The obtained camera is constrained by the [options] used when +/// querying the media input in [_getMediaStream]. +/// +/// The camera stream is displayed in the [videoElement] wrapped in the +/// [divElement] to avoid overriding the custom styles applied to +/// the video element in [_applyDefaultVideoStyles]. +/// See: https://github.com/flutter/flutter/issues/79519 +/// +/// The camera can be played/stopped by calling [play]/[stop] +/// or may capture a picture by [takePicture]. +/// +/// The [textureId] is used to register a camera view with the id +/// returned by [_getViewType]. +class Camera { + /// Creates a new instance of [Camera] + /// with the given [textureId] and optional + /// [options] and [window]. + Camera({ + required this.textureId, + this.options = const CameraOptions(), + html.Window? window, + }) : window = window ?? html.window; + + /// The texture id used to register the camera view. + final int textureId; + + /// The camera options used to initialize a camera, empty by default. + final CameraOptions options; + + /// The current browser window used to access device cameras. + final html.Window window; + + /// The video element that displays the camera stream. + /// Initialized in [initialize]. + late html.VideoElement videoElement; + + /// The wrapping element for the [videoElement] to avoid overriding + /// the custom styles applied in [_applyDefaultVideoStyles]. + /// Initialized in [initialize]. + late html.DivElement divElement; + + /// Initializes the camera stream displayed in the [videoElement]. + /// Registers the camera view with [textureId] under [_getViewType] type. + Future initialize() async { + final isSupported = window.navigator.mediaDevices?.getUserMedia != null; + if (!isSupported) { + throw CameraException( + CameraErrorCodes.notSupported, + 'The camera is not supported on this device.', + ); + } + + videoElement = html.VideoElement(); + _applyDefaultVideoStyles(videoElement); + + divElement = html.DivElement() + ..style.setProperty('object-fit', 'cover') + ..append(videoElement); + + ui.platformViewRegistry.registerViewFactory( + _getViewType(textureId), + (_) => divElement, + ); + + final stream = await _getMediaStream(); + videoElement + ..autoplay = false + ..muted = !options.audio.enabled + ..srcObject = stream + ..setAttribute('playsinline', ''); + } + + Future _getMediaStream() async { + try { + final constraints = await options.toJson(); + return await window.navigator.mediaDevices!.getUserMedia(constraints); + } on html.DomException catch (e) { + switch (e.name) { + case 'NotFoundError': + case 'DevicesNotFoundError': + throw CameraException( + CameraErrorCodes.notFound, + 'No camera found for the given camera options.', + ); + case 'NotReadableError': + case 'TrackStartError': + throw CameraException( + CameraErrorCodes.notReadable, + 'The camera is not readable due to a hardware error ' + 'that prevented access to the device.', + ); + case 'OverconstrainedError': + case 'ConstraintNotSatisfiedError': + throw CameraException( + CameraErrorCodes.overconstrained, + 'The camera options are impossible to satisfy.', + ); + case 'NotAllowedError': + case 'PermissionDeniedError': + throw CameraException( + CameraErrorCodes.permissionDenied, + 'The camera cannot be used or the permission ' + 'to access the camera is not granted.', + ); + case 'TypeError': + throw CameraException( + CameraErrorCodes.type, + 'The camera options are incorrect or attempted' + 'to access the media input from an insecure context.', + ); + default: + throw CameraException( + CameraErrorCodes.unknown, + 'An unknown error occured when initializing the camera.', + ); + } + } catch (_) { + throw CameraException( + CameraErrorCodes.unknown, + 'An unknown error occured when initializing the camera.', + ); + } + } + + /// Starts the camera stream. + /// + /// Initializes the camera source if the camera was previously stopped. + Future play() async { + if (videoElement.srcObject == null) { + final stream = await _getMediaStream(); + videoElement.srcObject = stream; + } + await videoElement.play(); + } + + /// Stops the camera stream and resets the camera source. + void stop() { + final tracks = videoElement.srcObject?.getTracks(); + if (tracks != null) { + for (final track in tracks) { + track.stop(); + } + } + videoElement.srcObject = null; + } + + /// Captures a picture and returns the saved file in a JPEG format. + Future takePicture() async { + final videoWidth = videoElement.videoWidth; + final videoHeight = videoElement.videoHeight; + final canvas = html.CanvasElement(width: videoWidth, height: videoHeight); + canvas.context2D + ..translate(videoWidth, 0) + ..scale(-1, 1) + ..drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight); + final blob = await canvas.toBlob('image/jpeg'); + return XFile(html.Url.createObjectUrl(blob)); + } + + /// Disposes the camera by stopping the camera stream + /// and reloading the camera source. + void dispose() { + /// Stop the camera stream. + stop(); + + /// Reset the [videoElement] to its initial state. + videoElement + ..srcObject = null + ..load(); + } + + /// Applies default styles to the video [element]. + void _applyDefaultVideoStyles(html.VideoElement element) { + element.style + ..transformOrigin = 'center' + ..pointerEvents = 'none' + ..width = '100%' + ..height = '100%' + ..objectFit = 'cover' + ..transform = 'scaleX(-1)'; + } +} diff --git a/packages/camera/camera_web/lib/src/shims/dart_ui.dart b/packages/camera/camera_web/lib/src/shims/dart_ui.dart new file mode 100644 index 000000000000..5eacec5fe867 --- /dev/null +++ b/packages/camera/camera_web/lib/src/shims/dart_ui.dart @@ -0,0 +1,10 @@ +// 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. + +/// This file shims dart:ui in web-only scenarios, getting rid of the need to +/// suppress analyzer warnings. + +// TODO(flutter/flutter#55000) Remove this file once web-only dart:ui APIs +// are exposed from a dedicated place. +export 'dart_ui_fake.dart' if (dart.library.html) 'dart_ui_real.dart'; diff --git a/packages/camera/camera_web/lib/src/shims/dart_ui_fake.dart b/packages/camera/camera_web/lib/src/shims/dart_ui_fake.dart new file mode 100644 index 000000000000..f2862af8b704 --- /dev/null +++ b/packages/camera/camera_web/lib/src/shims/dart_ui_fake.dart @@ -0,0 +1,28 @@ +// 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 'dart:html' as html; + +// Fake interface for the logic that this package needs from (web-only) dart:ui. +// This is conditionally exported so the analyzer sees these methods as available. + +/// Shim for web_ui engine.PlatformViewRegistry +/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62 +class platformViewRegistry { + /// Shim for registerViewFactory + /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72 + static registerViewFactory( + String viewTypeId, html.Element Function(int viewId) viewFactory) {} +} + +/// Shim for web_ui engine.AssetManager. +/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12 +class webOnlyAssetManager { + /// Shim for getAssetUrl. + /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L45 + static getAssetUrl(String asset) {} +} + +/// Signature of callbacks that have no arguments and return no data. +typedef VoidCallback = void Function(); diff --git a/packages/camera/camera_web/lib/src/shims/dart_ui_real.dart b/packages/camera/camera_web/lib/src/shims/dart_ui_real.dart new file mode 100644 index 000000000000..276b768c76c5 --- /dev/null +++ b/packages/camera/camera_web/lib/src/shims/dart_ui_real.dart @@ -0,0 +1,5 @@ +// 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. + +export 'dart:ui'; diff --git a/packages/camera/camera_web/lib/src/types/camera_error_codes.dart b/packages/camera/camera_web/lib/src/types/camera_error_codes.dart new file mode 100644 index 000000000000..f8dc5dfc4e32 --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/camera_error_codes.dart @@ -0,0 +1,29 @@ +// 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. + +/// Error codes that may occur during the camera initialization or streaming. +abstract class CameraErrorCodes { + /// The camera is not supported. + static const notSupported = 'cameraNotSupported'; + + /// The camera is not found. + static const notFound = 'cameraNotFound'; + + /// The camera is not readable. + static const notReadable = 'cameraNotReadable'; + + /// The camera options are impossible to satisfy. + static const overconstrained = 'cameraOverconstrained'; + + /// The camera cannot be used or the permission + /// to access the camera is not granted. + static const permissionDenied = 'cameraPermission'; + + /// The camera options are incorrect or attempted + /// to access the media input from an insecure context. + static const type = 'cameraType'; + + /// An unknown camera error. + static const unknown = 'cameraUnknown'; +} diff --git a/packages/camera/camera_web/lib/src/types/types.dart b/packages/camera/camera_web/lib/src/types/types.dart index deccd32da4c0..fc1f931679ff 100644 --- a/packages/camera/camera_web/lib/src/types/types.dart +++ b/packages/camera/camera_web/lib/src/types/types.dart @@ -2,4 +2,5 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'camera_error_codes.dart'; export 'camera_options.dart'; From 09ec5192f3ce31622631458ba9655cc257a07951 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 20 Jul 2021 18:17:20 -0700 Subject: [PATCH 016/123] [flutter_plugin_tests] Split analyze out of xctest (#4161) To prep for making a combined command to run native tests across different platforms, rework `xctest`: - Split analyze out into a new `xcode-analyze` command: - Since the analyze step runs a new build over everything with different flags, this is only a small amount slower than the combined version - This makes the logic easier to follow - This allows us to meaningfully report skips, to better notice missing tests. - Add the ability to target specific test bundles (RunnerTests or RunnerUITests) To share code between the commands, this extracts a new `Xcode` helper class. Part of https://github.com/flutter/flutter/issues/84392 and https://github.com/flutter/flutter/issues/86489 --- .cirrus.yml | 4 + script/tool/CHANGELOG.md | 6 + script/tool/lib/src/common/xcode.dart | 159 +++++++ script/tool/lib/src/main.dart | 2 + .../tool/lib/src/xcode_analyze_command.dart | 111 +++++ script/tool/lib/src/xctest_command.dart | 219 ++++----- script/tool/test/common/xcode_test.dart | 396 ++++++++++++++++ .../tool/test/xcode_analyze_command_test.dart | 416 +++++++++++++++++ script/tool/test/xctest_command_test.dart | 427 +++++++++++------- 9 files changed, 1444 insertions(+), 296 deletions(-) create mode 100644 script/tool/lib/src/common/xcode.dart create mode 100644 script/tool/lib/src/xcode_analyze_command.dart create mode 100644 script/tool/test/common/xcode_test.dart create mode 100644 script/tool/test/xcode_analyze_command_test.dart diff --git a/.cirrus.yml b/.cirrus.yml index 8f69bd188c06..bf5675b6e3ae 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -221,6 +221,8 @@ task: - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-11 com.apple.CoreSimulator.SimRuntime.iOS-14-5 | xargs xcrun simctl boot build_script: - ./script/tool_runner.sh build-examples --ios + xcode_analyze_script: + - ./script/tool_runner.sh xcode-analyze --ios xctest_script: - ./script/tool_runner.sh xctest --ios --ios-destination "platform=iOS Simulator,name=iPhone 11,OS=latest" drive_script: @@ -249,6 +251,8 @@ task: build_script: - flutter config --enable-macos-desktop - ./script/tool_runner.sh build-examples --macos + xcode_analyze_script: + - ./script/tool_runner.sh xcode-analyze --macos xctest_script: - ./script/tool_runner.sh xctest --macos --exclude $PLUGINS_TO_EXCLUDE_MACOS_XCTESTS drive_script: diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 1e447721d13f..377e7860bd26 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,9 @@ +## NEXT + +- Added an `xctest` flag to select specific test targets, to allow running only + unit tests or integration tests. +- Split Xcode analysis out of `xctest` and into a new `xcode-analyze` command. + ## 0.4.1 - Improved `license-check` output. diff --git a/script/tool/lib/src/common/xcode.dart b/script/tool/lib/src/common/xcode.dart new file mode 100644 index 000000000000..d6bbae419eda --- /dev/null +++ b/script/tool/lib/src/common/xcode.dart @@ -0,0 +1,159 @@ +// 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 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; + +import 'core.dart'; +import 'process_runner.dart'; + +const String _xcodeBuildCommand = 'xcodebuild'; +const String _xcRunCommand = 'xcrun'; + +/// A utility class for interacting with the installed version of Xcode. +class Xcode { + /// Creates an instance that runs commends with the given [processRunner]. + /// + /// If [log] is true, commands run by this instance will long various status + /// messages. + Xcode({ + this.processRunner = const ProcessRunner(), + this.log = false, + }); + + /// The [ProcessRunner] used to run commands. Overridable for testing. + final ProcessRunner processRunner; + + /// Whether or not to log when running commands. + final bool log; + + /// Runs an `xcodebuild` in [directory] with the given parameters. + Future runXcodeBuild( + Directory directory, { + List actions = const ['build'], + required String workspace, + required String scheme, + String? configuration, + List extraFlags = const [], + }) { + final List args = [ + _xcodeBuildCommand, + ...actions, + if (workspace != null) ...['-workspace', workspace], + if (scheme != null) ...['-scheme', scheme], + if (configuration != null) ...['-configuration', configuration], + ...extraFlags, + ]; + final String completeTestCommand = '$_xcRunCommand ${args.join(' ')}'; + if (log) { + print(completeTestCommand); + } + return processRunner.runAndStream(_xcRunCommand, args, + workingDir: directory); + } + + /// Returns true if [project], which should be an .xcodeproj directory, + /// contains a target called [target], false if it does not, and null if the + /// check fails (e.g., if [project] is not an Xcode project). + Future projectHasTarget(Directory project, String target) async { + final io.ProcessResult result = + await processRunner.run(_xcRunCommand, [ + _xcodeBuildCommand, + '-list', + '-json', + '-project', + project.path, + ]); + if (result.exitCode != 0) { + return null; + } + Map? projectInfo; + try { + projectInfo = (jsonDecode(result.stdout as String) + as Map)['project'] as Map?; + } on FormatException { + return null; + } + if (projectInfo == null) { + return null; + } + final List? targets = + (projectInfo['targets'] as List?)?.cast(); + return targets?.contains(target) ?? false; + } + + /// Returns the newest available simulator (highest OS version, with ties + /// broken in favor of newest device), if any. + Future findBestAvailableIphoneSimulator() async { + final List findSimulatorsArguments = [ + 'simctl', + 'list', + 'devices', + 'runtimes', + 'available', + '--json', + ]; + final String findSimulatorCompleteCommand = + '$_xcRunCommand ${findSimulatorsArguments.join(' ')}'; + if (log) { + print('Looking for available simulators...'); + print(findSimulatorCompleteCommand); + } + final io.ProcessResult findSimulatorsResult = + await processRunner.run(_xcRunCommand, findSimulatorsArguments); + if (findSimulatorsResult.exitCode != 0) { + if (log) { + printError( + 'Error occurred while running "$findSimulatorCompleteCommand":\n' + '${findSimulatorsResult.stderr}'); + } + return null; + } + final Map simulatorListJson = + jsonDecode(findSimulatorsResult.stdout as String) + as Map; + final List> runtimes = + (simulatorListJson['runtimes'] as List) + .cast>(); + final Map devices = + (simulatorListJson['devices'] as Map) + .cast(); + if (runtimes.isEmpty || devices.isEmpty) { + return null; + } + String? id; + // Looking for runtimes, trying to find one with highest OS version. + for (final Map rawRuntimeMap in runtimes.reversed) { + final Map runtimeMap = + rawRuntimeMap.cast(); + if ((runtimeMap['name'] as String?)?.contains('iOS') != true) { + continue; + } + final String? runtimeID = runtimeMap['identifier'] as String?; + if (runtimeID == null) { + continue; + } + final List>? devicesForRuntime = + (devices[runtimeID] as List?)?.cast>(); + if (devicesForRuntime == null || devicesForRuntime.isEmpty) { + continue; + } + // Looking for runtimes, trying to find latest version of device. + for (final Map rawDevice in devicesForRuntime.reversed) { + final Map device = rawDevice.cast(); + id = device['udid'] as String?; + if (id == null) { + continue; + } + if (log) { + print('device selected: $device'); + } + return id; + } + } + return null; + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index f397a04aa663..ef1a18ab15b2 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -24,6 +24,7 @@ import 'publish_plugin_command.dart'; import 'pubspec_check_command.dart'; import 'test_command.dart'; import 'version_check_command.dart'; +import 'xcode_analyze_command.dart'; import 'xctest_command.dart'; void main(List args) { @@ -59,6 +60,7 @@ void main(List args) { ..addCommand(PubspecCheckCommand(packagesDir)) ..addCommand(TestCommand(packagesDir)) ..addCommand(VersionCheckCommand(packagesDir)) + ..addCommand(XcodeAnalyzeCommand(packagesDir)) ..addCommand(XCTestCommand(packagesDir)); commandRunner.run(args).catchError((Object e) { diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart new file mode 100644 index 000000000000..27cd8c435142 --- /dev/null +++ b/script/tool/lib/src/xcode_analyze_command.dart @@ -0,0 +1,111 @@ +// 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:file/file.dart'; +import 'package:platform/platform.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; +import 'common/xcode.dart'; + +/// The command to run Xcode's static analyzer on plugins. +class XcodeAnalyzeCommand extends PackageLoopingCommand { + /// Creates an instance of the test command. + XcodeAnalyzeCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : _xcode = Xcode(processRunner: processRunner, log: true), + super(packagesDir, processRunner: processRunner, platform: platform) { + argParser.addFlag(kPlatformIos, help: 'Analyze iOS'); + argParser.addFlag(kPlatformMacos, help: 'Analyze macOS'); + } + + final Xcode _xcode; + + @override + final String name = 'xcode-analyze'; + + @override + final String description = + 'Runs Xcode analysis on the iOS and/or macOS example apps.'; + + @override + Future initializeRun() async { + if (!(getBoolArg(kPlatformIos) || getBoolArg(kPlatformMacos))) { + printError('At least one platform flag must be provided.'); + throw ToolExit(exitInvalidArguments); + } + } + + @override + Future runForPackage(Directory package) async { + final bool testIos = getBoolArg(kPlatformIos) && + pluginSupportsPlatform(kPlatformIos, package, + requiredMode: PlatformSupport.inline); + final bool testMacos = getBoolArg(kPlatformMacos) && + pluginSupportsPlatform(kPlatformMacos, package, + requiredMode: PlatformSupport.inline); + + final bool multiplePlatformsRequested = + getBoolArg(kPlatformIos) && getBoolArg(kPlatformMacos); + if (!(testIos || testMacos)) { + return PackageResult.skip('Not implemented for target platform(s).'); + } + + final List failures = []; + if (testIos && + !await _analyzePlugin(package, 'iOS', extraFlags: [ + '-destination', + 'generic/platform=iOS Simulator' + ])) { + failures.add('iOS'); + } + if (testMacos && !await _analyzePlugin(package, 'macOS')) { + failures.add('macOS'); + } + + // Only provide the failing platform in the failure details if testing + // multiple platforms, otherwise it's just noise. + return failures.isEmpty + ? PackageResult.success() + : PackageResult.fail( + multiplePlatformsRequested ? failures : []); + } + + /// Analyzes [plugin] for [platform], returning true if it passed analysis. + Future _analyzePlugin( + Directory plugin, + String platform, { + List extraFlags = const [], + }) async { + bool passing = true; + for (final Directory example in getExamplesForPlugin(plugin)) { + // Running tests and static analyzer. + final String examplePath = + getRelativePosixPath(example, from: plugin.parent); + print('Running $platform tests and analyzer for $examplePath...'); + final int exitCode = await _xcode.runXcodeBuild( + example, + actions: ['analyze'], + workspace: '${platform.toLowerCase()}/Runner.xcworkspace', + scheme: 'Runner', + configuration: 'Debug', + extraFlags: [ + ...extraFlags, + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + ); + if (exitCode == 0) { + printSuccess('$examplePath ($platform) passed analysis.'); + } else { + printError('$examplePath ($platform) failed analysis.'); + passing = false; + } + } + return passing; + } +} diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index 176adad39a09..44fc3a87d540 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:convert'; -import 'dart:io' as io; - import 'package:file/file.dart'; import 'package:platform/platform.dart'; @@ -12,35 +9,39 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/xcode.dart'; + +const String _iosDestinationFlag = 'ios-destination'; +const String _testTargetFlag = 'test-target'; -const String _kiOSDestination = 'ios-destination'; -const String _kXcodeBuildCommand = 'xcodebuild'; -const String _kXCRunCommand = 'xcrun'; -const String _kFoundNoSimulatorsMessage = - 'Cannot find any available simulators, tests failed'; +// The exit code from 'xcodebuild test' when there are no tests. +const int _xcodebuildNoTestExitCode = 66; -const int _exitFindingSimulatorsFailed = 3; -const int _exitNoSimulators = 4; +const int _exitNoSimulators = 3; /// The command to run XCTests (XCUnitTest and XCUITest) in plugins. /// The tests target have to be added to the Xcode project of the example app, /// usually at "example/{ios,macos}/Runner.xcworkspace". -/// -/// The static analyzer is also run. class XCTestCommand extends PackageLoopingCommand { /// Creates an instance of the test command. XCTestCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { + }) : _xcode = Xcode(processRunner: processRunner, log: true), + super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addOption( - _kiOSDestination, + _iosDestinationFlag, help: 'Specify the destination when running the test, used for -destination flag for xcodebuild command.\n' 'this is passed to the `-destination` argument in xcodebuild command.\n' 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', ); + argParser.addOption( + _testTargetFlag, + help: + 'Limits the tests to a specific target (e.g., RunnerTests or RunnerUITests)', + ); argParser.addFlag(kPlatformIos, help: 'Runs the iOS tests'); argParser.addFlag(kPlatformMacos, help: 'Runs the macOS tests'); } @@ -48,6 +49,8 @@ class XCTestCommand extends PackageLoopingCommand { // The device destination flags for iOS tests. List _iosDestinationFlags = []; + final Xcode _xcode; + @override final String name = 'xctest'; @@ -56,9 +59,6 @@ class XCTestCommand extends PackageLoopingCommand { 'Runs the xctests in the iOS and/or macOS example apps.\n\n' 'This command requires "flutter" and "xcrun" to be in your path.'; - @override - String get failureListHeader => 'The following packages are failing XCTests:'; - @override Future initializeRun() async { final bool shouldTestIos = getBoolArg(kPlatformIos); @@ -70,11 +70,12 @@ class XCTestCommand extends PackageLoopingCommand { } if (shouldTestIos) { - String destination = getStringArg(_kiOSDestination); + String destination = getStringArg(_iosDestinationFlag); if (destination.isEmpty) { - final String? simulatorId = await _findAvailableIphoneSimulator(); + final String? simulatorId = + await _xcode.findBestAvailableIphoneSimulator(); if (simulatorId == null) { - printError(_kFoundNoSimulatorsMessage); + printError('Cannot find any available simulators, tests failed'); throw ToolExit(_exitNoSimulators); } destination = 'id=$simulatorId'; @@ -115,15 +116,26 @@ class XCTestCommand extends PackageLoopingCommand { } final List failures = []; - if (testIos && - !await _testPlugin(package, 'iOS', - extraXcrunFlags: _iosDestinationFlags)) { - failures.add('iOS'); + bool ranTests = false; + if (testIos) { + final RunState result = await _testPlugin(package, 'iOS', + extraXcrunFlags: _iosDestinationFlags); + ranTests |= result != RunState.skipped; + if (result == RunState.failed) { + failures.add('iOS'); + } } - if (testMacos && !await _testPlugin(package, 'macOS')) { - failures.add('macOS'); + if (testMacos) { + final RunState result = await _testPlugin(package, 'macOS'); + ranTests |= result != RunState.skipped; + if (result == RunState.failed) { + failures.add('macOS'); + } } + if (!ranTests) { + return PackageResult.skip('No tests found.'); + } // Only provide the failing platform in the failure details if testing // multiple platforms, otherwise it's just noise. return failures.isEmpty @@ -133,124 +145,67 @@ class XCTestCommand extends PackageLoopingCommand { } /// Runs all applicable tests for [plugin], printing status and returning - /// success if the tests passed. - Future _testPlugin( + /// the test result. + Future _testPlugin( Directory plugin, String platform, { List extraXcrunFlags = const [], }) async { - bool passing = true; + final String testTarget = getStringArg(_testTargetFlag); + + // Assume skipped until at least one test has run. + RunState overallResult = RunState.skipped; for (final Directory example in getExamplesForPlugin(plugin)) { - // Running tests and static analyzer. final String examplePath = getRelativePosixPath(example, from: plugin.parent); - print('Running $platform tests and analyzer for $examplePath...'); - int exitCode = - await _runTests(true, example, platform, extraFlags: extraXcrunFlags); - // 66 = there is no test target (this fails fast). Try again with just the analyzer. - if (exitCode == 66) { - print('Tests not found for $examplePath, running analyzer only...'); - exitCode = await _runTests(false, example, platform, - extraFlags: extraXcrunFlags); - } - if (exitCode == 0) { - printSuccess('Successfully ran $platform xctest for $examplePath'); - } else { - passing = false; - } - } - return passing; - } - Future _runTests( - bool runTests, - Directory example, - String platform, { - List extraFlags = const [], - }) { - final List xctestArgs = [ - _kXcodeBuildCommand, - if (runTests) 'test', - 'analyze', - '-workspace', - '${platform.toLowerCase()}/Runner.xcworkspace', - '-configuration', - 'Debug', - '-scheme', - 'Runner', - ...extraFlags, - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ]; - final String completeTestCommand = - '$_kXCRunCommand ${xctestArgs.join(' ')}'; - print(completeTestCommand); - return processRunner.runAndStream(_kXCRunCommand, xctestArgs, - workingDir: example); - } - - Future _findAvailableIphoneSimulator() async { - // Find the first available destination if not specified. - final List findSimulatorsArguments = [ - 'simctl', - 'list', - '--json' - ]; - final String findSimulatorCompleteCommand = - '$_kXCRunCommand ${findSimulatorsArguments.join(' ')}'; - print('Looking for available simulators...'); - print(findSimulatorCompleteCommand); - final io.ProcessResult findSimulatorsResult = - await processRunner.run(_kXCRunCommand, findSimulatorsArguments); - if (findSimulatorsResult.exitCode != 0) { - printError( - 'Error occurred while running "$findSimulatorCompleteCommand":\n' - '${findSimulatorsResult.stderr}'); - throw ToolExit(_exitFindingSimulatorsFailed); - } - final Map simulatorListJson = - jsonDecode(findSimulatorsResult.stdout as String) - as Map; - final List> runtimes = - (simulatorListJson['runtimes'] as List) - .cast>(); - final Map devices = - (simulatorListJson['devices'] as Map) - .cast(); - if (runtimes.isEmpty || devices.isEmpty) { - return null; - } - String? id; - // Looking for runtimes, trying to find one with highest OS version. - for (final Map rawRuntimeMap in runtimes.reversed) { - final Map runtimeMap = - rawRuntimeMap.cast(); - if ((runtimeMap['name'] as String?)?.contains('iOS') != true) { - continue; - } - final String? runtimeID = runtimeMap['identifier'] as String?; - if (runtimeID == null) { - continue; - } - final List>? devicesForRuntime = - (devices[runtimeID] as List?)?.cast>(); - if (devicesForRuntime == null || devicesForRuntime.isEmpty) { - continue; - } - // Looking for runtimes, trying to find latest version of device. - for (final Map rawDevice in devicesForRuntime.reversed) { - final Map device = rawDevice.cast(); - if (device['availabilityError'] != null || - (device['isAvailable'] as bool?) == false) { + if (testTarget.isNotEmpty) { + final Directory project = example + .childDirectory(platform.toLowerCase()) + .childDirectory('Runner.xcodeproj'); + final bool? hasTarget = + await _xcode.projectHasTarget(project, testTarget); + if (hasTarget == null) { + printError('Unable to check targets for $examplePath.'); + overallResult = RunState.failed; continue; - } - id = device['udid'] as String?; - if (id == null) { + } else if (!hasTarget) { + print('No "$testTarget" target in $examplePath; skipping.'); continue; } - print('device selected: $device'); - return id; + } + + print('Running $platform tests for $examplePath...'); + final int exitCode = await _xcode.runXcodeBuild( + example, + actions: ['test'], + workspace: '${platform.toLowerCase()}/Runner.xcworkspace', + scheme: 'Runner', + configuration: 'Debug', + extraFlags: [ + if (testTarget.isNotEmpty) '-only-testing:$testTarget', + ...extraXcrunFlags, + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + ); + + switch (exitCode) { + case _xcodebuildNoTestExitCode: + print('No tests found for $examplePath'); + continue; + case 0: + printSuccess('Successfully ran $platform xctest for $examplePath'); + // If this is the first test, assume success until something fails. + if (overallResult == RunState.skipped) { + overallResult = RunState.succeeded; + } + break; + default: + // Any failure means a failure overall. + overallResult = RunState.failed; + break; } } - return null; + return overallResult; } } diff --git a/script/tool/test/common/xcode_test.dart b/script/tool/test/common/xcode_test.dart new file mode 100644 index 000000000000..7e046a2446c2 --- /dev/null +++ b/script/tool/test/common/xcode_test.dart @@ -0,0 +1,396 @@ +// 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 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/common/xcode.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; +import '../util.dart'; + +void main() { + late RecordingProcessRunner processRunner; + late Xcode xcode; + + setUp(() { + processRunner = RecordingProcessRunner(); + xcode = Xcode(processRunner: processRunner); + }); + + group('findBestAvailableIphoneSimulator', () { + test('finds the newest device', () async { + const String expectedDeviceId = '1E76A0FD-38AC-4537-A989-EA639D7D012A'; + // Note: This uses `dynamic` deliberately, and should not be updated to + // Object, in order to ensure that the code correctly handles this return + // type from JSON decoding. + final Map devices = { + 'runtimes': >[ + { + 'bundlePath': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime', + 'buildversion': '17A577', + 'runtimeRoot': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-0', + 'version': '13.0', + 'isAvailable': true, + 'name': 'iOS 13.0' + }, + { + 'bundlePath': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', + 'buildversion': '17L255', + 'runtimeRoot': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4', + 'version': '13.4', + 'isAvailable': true, + 'name': 'iOS 13.4' + }, + { + 'bundlePath': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime', + 'buildversion': '17T531', + 'runtimeRoot': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2', + 'version': '6.2.1', + 'isAvailable': true, + 'name': 'watchOS 6.2' + } + ], + 'devices': { + 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774', + 'udid': '2706BBEB-1E01-403E-A8E9-70E8E5A24774', + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.iPhone-8', + 'state': 'Shutdown', + 'name': 'iPhone 8' + }, + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'udid': expectedDeviceId, + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus', + 'state': 'Shutdown', + 'name': 'iPhone 8 Plus' + } + ] + } + }; + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = jsonEncode(devices); + + expect(await xcode.findBestAvailableIphoneSimulator(), expectedDeviceId); + }); + + test('ignores non-iOS runtimes', () async { + // Note: This uses `dynamic` deliberately, and should not be updated to + // Object, in order to ensure that the code correctly handles this return + // type from JSON decoding. + final Map devices = { + 'runtimes': >[ + { + 'bundlePath': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime', + 'buildversion': '17T531', + 'runtimeRoot': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2', + 'version': '6.2.1', + 'isAvailable': true, + 'name': 'watchOS 6.2' + } + ], + 'devices': { + 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2': + >[ + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.Apple-Watch-38mm', + 'state': 'Shutdown', + 'name': 'Apple Watch' + } + ] + } + }; + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = jsonEncode(devices); + + expect(await xcode.findBestAvailableIphoneSimulator(), null); + }); + + test('returns null if simctl fails', () async { + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing(), + ]; + + expect(await xcode.findBestAvailableIphoneSimulator(), null); + }); + }); + + group('runXcodeBuild', () { + test('handles minimal arguments', () async { + final Directory directory = const LocalFileSystem().currentDirectory; + + final int exitCode = await xcode.runXcodeBuild( + directory, + workspace: 'A.xcworkspace', + scheme: 'AScheme', + ); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'build', + '-workspace', + 'A.xcworkspace', + '-scheme', + 'AScheme', + ], + directory.path), + ])); + }); + + test('handles all arguments', () async { + final Directory directory = const LocalFileSystem().currentDirectory; + + final int exitCode = await xcode.runXcodeBuild(directory, + actions: ['action1', 'action2'], + workspace: 'A.xcworkspace', + scheme: 'AScheme', + configuration: 'Debug', + extraFlags: ['-a', '-b', 'c=d']); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'action1', + 'action2', + '-workspace', + 'A.xcworkspace', + '-scheme', + 'AScheme', + '-configuration', + 'Debug', + '-a', + '-b', + 'c=d', + ], + directory.path), + ])); + }); + + test('returns error codes', () async { + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing(), + ]; + final Directory directory = const LocalFileSystem().currentDirectory; + + final int exitCode = await xcode.runXcodeBuild( + directory, + workspace: 'A.xcworkspace', + scheme: 'AScheme', + ); + + expect(exitCode, 1); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'build', + '-workspace', + 'A.xcworkspace', + '-scheme', + 'AScheme', + ], + directory.path), + ])); + }); + }); + + group('projectHasTarget', () { + test('returns true when present', () async { + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = ''' +{ + "project" : { + "configurations" : [ + "Debug", + "Release" + ], + "name" : "Runner", + "schemes" : [ + "Runner" + ], + "targets" : [ + "Runner", + "RunnerTests", + "RunnerUITests" + ] + } +}'''; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), true); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns false when not present', () async { + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = ''' +{ + "project" : { + "configurations" : [ + "Debug", + "Release" + ], + "name" : "Runner", + "schemes" : [ + "Runner" + ], + "targets" : [ + "Runner", + "RunnerUITests" + ] + } +}'''; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), false); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns null for unexpected output', () async { + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = '{}'; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns null for invalid output', () async { + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = ':)'; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns null for failure', () async { + processRunner.processToReturn = MockProcess.failing(); + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + }); +} diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart new file mode 100644 index 000000000000..b715ac531f50 --- /dev/null +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -0,0 +1,416 @@ +// 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 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/xcode_analyze_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of +// doing all the process mocking and validation. +void main() { + group('test xcode_analyze_command', () { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(isMacOS: true); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final XcodeAnalyzeCommand command = XcodeAnalyzeCommand(packagesDir, + processRunner: processRunner, platform: mockPlatform); + + runner = CommandRunner( + 'xcode_analyze_command', 'Test for xcode_analyze_command'); + runner.addCommand(command); + }); + + test('Fails if no platforms are provided', () async { + Error? commandError; + final List output = await runCapturingPrint( + runner, ['xcode-analyze'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('At least one platform flag must be provided'), + ]), + ); + }); + + group('iOS', () { + test('skip if iOS is not supported', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final List output = + await runCapturingPrint(runner, ['xcode-analyze', '--ios']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if iOS is implemented in a federated package', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.federated + }); + + final List output = + await runCapturingPrint(runner, ['xcode-analyze', '--ios']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('runs for iOS plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('plugin/example (iOS) passed analysis.') + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'generic/platform=iOS Simulator', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('fails if xcrun fails', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'xcode-analyze', + '--ios', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin'), + ])); + }); + }); + + group('macOS', () { + test('skip if macOS is not supported', () async { + createFakePlugin( + 'plugin', + packagesDir, + ); + + final List output = await runCapturingPrint( + runner, ['xcode-analyze', '--macos']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if macOS is implemented in a federated package', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.federated, + }); + + final List output = await runCapturingPrint( + runner, ['xcode-analyze', '--macos']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('runs for macOS plugin', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--macos', + ]); + + expect(output, + contains(contains('plugin/example (macOS) passed analysis.'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('fails if xcrun fails', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['xcode-analyze', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin'), + ]), + ); + }); + }); + + group('combined', () { + test('runs both iOS and macOS when supported', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.inline, + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAll([ + contains('plugin/example (iOS) passed analysis.'), + contains('plugin/example (macOS) passed analysis.'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'generic/platform=iOS Simulator', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('runs only macOS for a macOS plugin', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAllInOrder([ + contains('plugin/example (macOS) passed analysis.'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('runs only iOS for a iOS plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAllInOrder( + [contains('plugin/example (iOS) passed analysis.')])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'generic/platform=iOS Simulator', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('skips when neither are supported', () async { + createFakePlugin('plugin', packagesDir); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAllInOrder([ + contains('SKIPPING: Not implemented for target platform(s).'), + ])); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + }); + }); +} diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index aa6d23fb56f5..324dea0e71ef 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -16,22 +16,8 @@ import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; -// Note: This uses `dynamic` deliberately, and should not be updated to Object, -// in order to ensure that the code correctly handles this return type from -// JSON decoding. final Map _kDeviceListMap = { 'runtimes': >[ - { - 'bundlePath': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime', - 'buildversion': '17A577', - 'runtimeRoot': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-0', - 'version': '13.0', - 'isAvailable': true, - 'name': 'iOS 13.0' - }, { 'bundlePath': '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', @@ -43,32 +29,9 @@ final Map _kDeviceListMap = { 'isAvailable': true, 'name': 'iOS 13.4' }, - { - 'bundlePath': - '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime', - 'buildversion': '17T531', - 'runtimeRoot': - '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2', - 'version': '6.2.1', - 'isAvailable': true, - 'name': 'watchOS 6.2' - } ], 'devices': { 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ - { - 'dataPath': - '/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data', - 'logPath': - '/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774', - 'udid': '2706BBEB-1E01-403E-A8E9-70E8E5A24774', - 'isAvailable': true, - 'deviceTypeIdentifier': - 'com.apple.CoreSimulator.SimDeviceType.iPhone-8', - 'state': 'Shutdown', - 'name': 'iPhone 8' - }, { 'dataPath': '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', @@ -85,6 +48,8 @@ final Map _kDeviceListMap = { } }; +// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of +// doing all the process mocking and validation. void main() { const String _kDestination = '--ios-destination'; @@ -123,13 +88,198 @@ void main() { ); }); + test('allows target filtering', () async { + final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = '{"project":{"targets":["RunnerTests"]}}'; + + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--macos', + '--test-target=RunnerTests', + ]); + + expect( + output, + contains( + contains('Successfully ran macOS xctest for plugin/example'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-only-testing:RunnerTests', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('skips when the requested target is not present', () async { + final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = '{"project":{"targets":["Runner"]}}'; + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--macos', + '--test-target=RunnerTests', + ]); + + expect( + output, + containsAllInOrder([ + contains('No "RunnerTests" target in plugin/example; skipping.'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ])); + }); + + test('fails if unable to check for requested target', () async { + final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.failing(); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--macos', + '--test-target=RunnerTests', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to check targets for plugin/example.'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ])); + }); + + test('reports skips with no tests', () async { + final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + // Exit code 66 from testing indicates no tests. + final MockProcess noTestsProcessResult = MockProcess(); + noTestsProcessResult.exitCodeCompleter.complete(66); + processRunner.mockProcessesForExecutable['xcrun'] = [ + noTestsProcessResult, + ]; + final List output = + await runCapturingPrint(runner, ['xctest', '--macos']); + + expect(output, contains(contains('No tests found.'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + group('iOS', () { test('skip if iOS is not supported', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); final List output = await runCapturingPrint(runner, ['xctest', '--ios', _kDestination, 'foo_destination']); @@ -141,11 +291,10 @@ void main() { }); test('skip if iOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformIos: PlatformSupport.federated - }); + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.federated + }); final List output = await runCapturingPrint(runner, ['xctest', '--ios', _kDestination, 'foo_destination']); @@ -157,19 +306,14 @@ void main() { }); test('running with correct destination', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { kPlatformIos: PlatformSupport.inline }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--ios', @@ -192,13 +336,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'ios/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', '-destination', 'foo_destination', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', @@ -209,45 +352,43 @@ void main() { test('Not specifying --ios-destination assigns an available simulator', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { kPlatformIos: PlatformSupport.inline }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final Map schemeCommandResult = { - 'project': { - 'targets': ['bar_scheme', 'foo_scheme'] - } - }; processRunner.processToReturn = MockProcess.succeeding(); - // For simplicity of the test, we combine all the mock results into a single mock result, each internal command - // will get this result and they should still be able to parse them correctly. - processRunner.resultStdout = - jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); + processRunner.resultStdout = jsonEncode(_kDeviceListMap); await runCapturingPrint(runner, ['xctest', '--ios']); expect( processRunner.recordedCalls, orderedEquals([ const ProcessCall( - 'xcrun', ['simctl', 'list', '--json'], null), + 'xcrun', + [ + 'simctl', + 'list', + 'devices', + 'runtimes', + 'available', + '--json', + ], + null), ProcessCall( 'xcrun', const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'ios/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', '-destination', 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', @@ -257,15 +398,11 @@ void main() { }); test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformIos: PlatformSupport.inline - }); + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.inline + }); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; processRunner.mockProcessesForExecutable['xcrun'] = [ MockProcess.failing() ]; @@ -288,7 +425,7 @@ void main() { expect( output, containsAllInOrder([ - contains('The following packages are failing XCTests:'), + contains('The following packages had errors:'), contains(' plugin'), ])); }); @@ -296,16 +433,10 @@ void main() { group('macOS', () { test('skip if macOS is not supported', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test', - ], - ); + createFakePlugin('plugin', packagesDir); - final List output = await runCapturingPrint(runner, - ['xctest', '--macos', _kDestination, 'foo_destination']); + final List output = + await runCapturingPrint(runner, ['xctest', '--macos']); expect( output, contains( @@ -314,14 +445,13 @@ void main() { }); test('skip if macOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformMacos: PlatformSupport.federated, - }); + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.federated, + }); - final List output = await runCapturingPrint(runner, - ['xctest', '--macos', _kDestination, 'foo_destination']); + final List output = + await runCapturingPrint(runner, ['xctest', '--macos']); expect( output, contains( @@ -330,19 +460,15 @@ void main() { }); test('runs for macOS plugin', () async { - final Directory pluginDirectory1 = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--macos', @@ -361,13 +487,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'macos/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ], pluginExampleDirectory.path), @@ -375,15 +500,11 @@ void main() { }); test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; processRunner.mockProcessesForExecutable['xcrun'] = [ MockProcess.failing() ]; @@ -398,7 +519,7 @@ void main() { expect( output, containsAllInOrder([ - contains('The following packages are failing XCTests:'), + contains('The following packages had errors:'), contains(' plugin'), ]), ); @@ -407,20 +528,16 @@ void main() { group('combined', () { test('runs both iOS and macOS when supported', () async { - final Directory pluginDirectory1 = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformIos: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, - }); + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.inline, + kPlatformMacos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--ios', @@ -444,13 +561,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'ios/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', '-destination', 'foo_destination', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', @@ -461,13 +577,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'macos/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ], pluginExampleDirectory.path), @@ -475,19 +590,15 @@ void main() { }); test('runs only macOS for a macOS plugin', () async { - final Directory pluginDirectory1 = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--ios', @@ -511,13 +622,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'macos/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ], pluginExampleDirectory.path), @@ -525,19 +635,14 @@ void main() { }); test('runs only iOS for a iOS plugin', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { kPlatformIos: PlatformSupport.inline }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--ios', @@ -561,13 +666,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'ios/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', '-destination', 'foo_destination', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', @@ -577,13 +681,8 @@ void main() { }); test('skips when neither are supported', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ]); + createFakePlugin('plugin', packagesDir); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--ios', From 44d7c072dfdb0aa83600606c7da9f806cefd0bd8 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 21 Jul 2021 12:03:47 -0700 Subject: [PATCH 017/123] [flutter_plugin_tools] Make firebase-test-lab fail when no tests run (#4172) If a package supports Android, it will now report failure instead of skip if no tests run. This matches the new behavior of drive-examples, and is intended to prevent recurrance of situations where we are silently failing to run tests because of, e.g., tests being in the wrong directory. Also fixes a long-standing but unnoticed problem where if a run tried to run more than one package's tests, it would hang forever (although on the bots it doesn't seem to time out, just end logs abruptly) due to a logic error in the call to configure gcloud. Fixes flutter/flutter#86732 --- .cirrus.yml | 18 +- script/tool/CHANGELOG.md | 6 + .../lib/src/firebase_test_lab_command.dart | 36 ++-- .../test/firebase_test_lab_command_test.dart | 160 +++++++++++++++++- 4 files changed, 202 insertions(+), 18 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index bf5675b6e3ae..96902cfd6d15 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -137,6 +137,22 @@ task: CHANNEL: "stable" MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[07586610af1fdfc894e5969f70ef2458341b9b7e9c3b7c4225a663b4a48732b7208a4d91c3b7d45305a6b55fa2a37fc4] + # Currently missing harness files (https://github.com/flutter/flutter/issues/86749): + # camera/camera + # google_sign_in/google_sign_in + # in_app_purchase/in_app_purchase + # in_app_purchase_android + # quick_actions + # shared_preferences/shared_preferences + # url_launcher/url_launcher + # video_player/video_player + # webview_flutter + # Deprecated; no plan to backfill the missing files: + # android_intent,connectivity/connectivity,device_info/device_info,sensors,share,wifi_info_flutter/wifi_info_flutter + # No integration tests to run: + # image_picker/image_picker - Native UI is the critical functionality + # espresso - No Dart code, so no integration tests + PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS: "camera/camera,google_sign_in/google_sign_in,in_app_purchase/in_app_purchase,in_app_purchase_android,quick_actions,shared_preferences/shared_preferences,url_launcher/url_launcher,video_player/video_player,webview_flutter,android_intent,connectivity/connectivity,device_info/device_info,sensors,share,wifi_info_flutter/wifi_info_flutter,image_picker/image_picker,espresso" build_script: # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they # might include non-ASCII characters which makes Gradle crash. @@ -159,7 +175,7 @@ task: - export CIRRUS_COMMIT_MESSAGE="" - if [[ -n "$GCLOUD_FIREBASE_TESTLAB_KEY" ]]; then - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json - - ./script/tool_runner.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26 + - ./script/tool_runner.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26 --exclude $PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS - else - echo "This user does not have permission to run Firebase Test Lab tests." - fi diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 377e7860bd26..d701278ee76f 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -3,6 +3,12 @@ - Added an `xctest` flag to select specific test targets, to allow running only unit tests or integration tests. - Split Xcode analysis out of `xctest` and into a new `xcode-analyze` command. +- Fixed a bug that caused `firebase-test-lab` to hang if it tried to run more + than one plugin's tests in a single run. +- **Breaking change**: If `firebase-test-lab` is run on a package that supports + Android, but for which no tests are run, it now fails instead of skipping. + This matches `drive-examples`, as this command is what is used for driving + Android Flutter integration tests on CI. ## 0.4.1 diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 5e4d9f080085..304912824960 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -76,13 +76,12 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { static const String _gradleWrapper = 'gradlew'; - Completer? _firebaseProjectConfigured; + bool _firebaseProjectConfigured = false; Future _configureFirebaseProject() async { - if (_firebaseProjectConfigured != null) { - return _firebaseProjectConfigured!.future; + if (_firebaseProjectConfigured) { + return; } - _firebaseProjectConfigured = Completer(); final String serviceKey = getStringArg('service-key'); if (serviceKey.isEmpty) { @@ -110,31 +109,34 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { print(''); if (exitCode == 0) { print('Firebase project configured.'); - return; } else { logWarning( 'Warning: gcloud config set returned a non-zero exit code. Continuing anyway.'); } } - _firebaseProjectConfigured!.complete(null); + _firebaseProjectConfigured = true; } @override Future runForPackage(Directory package) async { - if (!package - .childDirectory('example') - .childDirectory('android') + final Directory exampleDirectory = package.childDirectory('example'); + final Directory androidDirectory = + exampleDirectory.childDirectory('android'); + if (!androidDirectory.existsSync()) { + return PackageResult.skip( + '${getPackageDescription(exampleDirectory)} does not support Android.'); + } + + if (!androidDirectory .childDirectory('app') .childDirectory('src') .childDirectory('androidTest') .existsSync()) { - return PackageResult.skip('No example with androidTest directory'); + printError('No androidTest directory found.'); + return PackageResult.fail( + ['No tests ran (use --exclude if this is intentional).']); } - final Directory exampleDirectory = package.childDirectory('example'); - final Directory androidDirectory = - exampleDirectory.childDirectory('android'); - // Ensures that gradle wrapper exists if (!await _ensureGradleWrapperExists(androidDirectory)) { return PackageResult.fail(['Unable to build example apk']); @@ -191,6 +193,12 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { errors.add('$testName failed tests'); } } + + if (errors.isEmpty && resultsCounter == 0) { + printError('No integration tests were run.'); + errors.add('No tests ran (use --exclude if this is intentional).'); + } + return errors.isEmpty ? PackageResult.success() : PackageResult.fail(errors); diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index c265868bbf3e..185b9d83f0fe 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -84,6 +84,85 @@ void main() { ])); }); + test('only runs gcloud configuration once', () async { + createFakePlugin('plugin1', packagesDir, extraFiles: [ + 'test/plugin_test.dart', + 'example/integration_test/foo_test.dart', + 'example/android/gradlew', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + createFakePlugin('plugin2', packagesDir, extraFiles: [ + 'test/plugin_test.dart', + 'example/integration_test/bar_test.dart', + 'example/android/gradlew', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('Firebase project configured.'), + contains('Testing example/integration_test/foo_test.dart...'), + contains('Running for plugin2'), + contains('Testing example/integration_test/bar_test.dart...'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-infra'.split(' '), null), + ProcessCall( + '/packages/plugin1/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true'.split(' '), + '/packages/plugin1/example/android'), + ProcessCall( + '/packages/plugin1/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin1/example/integration_test/foo_test.dart' + .split(' '), + '/packages/plugin1/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin1/example'), + ProcessCall( + '/packages/plugin2/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true'.split(' '), + '/packages/plugin2/example/android'), + ProcessCall( + '/packages/plugin2/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin2/example/integration_test/bar_test.dart' + .split(' '), + '/packages/plugin2/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin2/example'), + ]), + ); + }); + test('runs integration tests', () async { createFakePlugin('plugin', packagesDir, extraFiles: [ 'test/plugin_test.dart', @@ -203,12 +282,87 @@ void main() { ); }); - test('skips packages with no androidTest directory', () async { + test('fails for packages with no androidTest directory', () async { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/android/gradlew', ]); + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No androidTest directory found.'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' No tests ran (use --exclude if this is intentional).'), + ]), + ); + }); + + test('fails for packages with no integration test files', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No integration tests were run'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' No tests ran (use --exclude if this is intentional).'), + ]), + ); + }); + + test('skips packages with no android directory', () async { + createFakePackage('package', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + ]); + final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', @@ -224,8 +378,8 @@ void main() { expect( output, containsAllInOrder([ - contains('Running for plugin'), - contains('No example with androidTest directory'), + contains('Running for package'), + contains('package/example does not support Android'), ]), ); expect(output, From fdded21a2145dd315d80145b1f6f2be5c8e8db49 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 22 Jul 2021 02:41:05 +0200 Subject: [PATCH 018/123] [camera_web] Add `availableCameras` implementation (#4175) --- .../camera_settings_test.dart | 210 +++++++++++++++ .../integration_test/camera_web_test.dart | 255 +++++++++++++++++- .../integration_test/helpers/mocks.dart | 41 ++- .../camera_web/lib/src/camera_settings.dart | 108 ++++++++ .../camera/camera_web/lib/src/camera_web.dart | 119 +++++++- .../lib/src/types/camera_metadata.dart | 37 +++ .../lib/src/types/media_device_kind.dart | 17 ++ .../camera_web/lib/src/types/types.dart | 2 + .../test/types/camera_metadata_test.dart | 25 ++ 9 files changed, 798 insertions(+), 16 deletions(-) create mode 100644 packages/camera/camera_web/example/integration_test/camera_settings_test.dart create mode 100644 packages/camera/camera_web/lib/src/camera_settings.dart create mode 100644 packages/camera/camera_web/lib/src/types/camera_metadata.dart create mode 100644 packages/camera/camera_web/lib/src/types/media_device_kind.dart create mode 100644 packages/camera/camera_web/test/types/camera_metadata_test.dart diff --git a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart new file mode 100644 index 000000000000..c1c00fe7a337 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart @@ -0,0 +1,210 @@ +// 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 'dart:html'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraSettings', () { + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + late CameraSettings settings; + + setUp(() async { + window = MockWindow(); + navigator = MockNavigator(); + mediaDevices = MockMediaDevices(); + + when(() => window.navigator).thenReturn(navigator); + when(() => navigator.mediaDevices).thenReturn(mediaDevices); + + settings = CameraSettings()..window = window; + }); + + group('getFacingModeForVideoTrack', () { + testWidgets( + 'throws CameraException ' + 'with notSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + expect( + () => settings.getFacingModeForVideoTrack(MockMediaStreamTrack()), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notSupported, + ), + ), + ); + }); + + testWidgets( + 'returns null ' + 'when the facing mode is not supported', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'facingMode': false, + }); + + final facingMode = + settings.getFacingModeForVideoTrack(MockMediaStreamTrack()); + + expect( + facingMode, + equals(null), + ); + }); + + group('when the facing mode is supported', () { + setUp(() { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'facingMode': true, + }); + }); + + testWidgets( + 'returns an appropriate facing mode ' + 'based on the video track settings', (tester) async { + final videoTrack = MockMediaStreamTrack(); + + when(videoTrack.getSettings).thenReturn({'facingMode': 'user'}); + + final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + + expect( + facingMode, + equals('user'), + ); + }); + + testWidgets( + 'returns an appropriate facing mode ' + 'based on the video track capabilities ' + 'when the facing mode setting is empty', (tester) async { + final videoTrack = MockMediaStreamTrack(); + + when(videoTrack.getSettings).thenReturn({}); + when(videoTrack.getCapabilities).thenReturn({ + 'facingMode': ['environment', 'left'] + }); + + final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + + expect( + facingMode, + equals('environment'), + ); + }); + + testWidgets( + 'returns null ' + 'when the facing mode setting ' + 'and capabilities are empty', (tester) async { + final videoTrack = MockMediaStreamTrack(); + + when(videoTrack.getSettings).thenReturn({}); + when(videoTrack.getCapabilities).thenReturn({'facingMode': []}); + + final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + + expect( + facingMode, + equals(null), + ); + }); + + testWidgets( + 'returns null ' + 'when the facing mode setting is empty and ' + 'the video track capabilities are not supported', (tester) async { + final videoTrack = MockMediaStreamTrack(); + + when(videoTrack.getSettings).thenReturn({}); + when(videoTrack.getCapabilities).thenThrow(JSNoSuchMethodError()); + + final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + + expect( + facingMode, + equals(null), + ); + }); + + testWidgets( + 'throws CameraException ' + 'with unknown error ' + 'when getting the video track capabilities ' + 'throws an unknown error', (tester) async { + final videoTrack = MockMediaStreamTrack(); + + when(videoTrack.getSettings).thenReturn({}); + when(videoTrack.getCapabilities).thenThrow(Exception('Unknown')); + + expect( + () => settings.getFacingModeForVideoTrack(videoTrack), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.unknown, + ), + ), + ); + }); + }); + }); + + group('mapFacingModeToLensDirection', () { + testWidgets( + 'returns front ' + 'when the facing mode is user', (tester) async { + expect( + settings.mapFacingModeToLensDirection('user'), + equals(CameraLensDirection.front), + ); + }); + + testWidgets( + 'returns back ' + 'when the facing mode is environment', (tester) async { + expect( + settings.mapFacingModeToLensDirection('environment'), + equals(CameraLensDirection.back), + ); + }); + + testWidgets( + 'returns external ' + 'when the facing mode is left', (tester) async { + expect( + settings.mapFacingModeToLensDirection('left'), + equals(CameraLensDirection.external), + ); + }); + + testWidgets( + 'returns external ' + 'when the facing mode is right', (tester) async { + expect( + settings.mapFacingModeToLensDirection('right'), + equals(CameraLensDirection.external), + ); + }); + }); + }); +} + +class JSNoSuchMethodError implements Exception {} diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index d26f0e855889..25368daf02f7 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -6,6 +6,8 @@ import 'dart:html'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/camera_web.dart'; +import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/types/types.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -23,6 +25,7 @@ void main() { late Navigator navigator; late MediaDevices mediaDevices; late VideoElement videoElement; + late CameraSettings cameraSettings; setUp(() async { window = MockWindow(); @@ -33,7 +36,10 @@ void main() { 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4' ..preload = 'true' ..width = 10 - ..height = 10; + ..height = 10 + ..crossOrigin = 'anonymous'; + + cameraSettings = MockCameraSettings(); when(() => window.navigator).thenReturn(navigator); when(() => navigator.mediaDevices).thenReturn(mediaDevices); @@ -41,18 +47,253 @@ void main() { () => mediaDevices.getUserMedia(any()), ).thenAnswer((_) async => videoElement.captureStream()); - CameraPlatform.instance = CameraPlugin()..window = window; + CameraPlatform.instance = CameraPlugin( + cameraSettings: cameraSettings, + )..window = window; + }); + + setUpAll(() { + registerFallbackValue(MockMediaStreamTrack()); }); testWidgets('CameraPlugin is the live instance', (tester) async { expect(CameraPlatform.instance, isA()); }); - testWidgets('availableCameras throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.availableCameras(), - throwsUnimplementedError, - ); + group('availableCameras', () { + setUp(() { + when( + () => cameraSettings.getFacingModeForVideoTrack( + any(), + ), + ).thenReturn(null); + }); + + testWidgets( + 'throws CameraException ' + 'with notSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + expect( + () => CameraPlatform.instance.availableCameras(), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notSupported, + ), + ), + ); + }); + + testWidgets( + 'calls MediaDevices.getUserMedia ' + 'on the video input device', (tester) async { + final videoDevice = FakeMediaDeviceInfo( + '1', + 'Camera 1', + MediaDeviceKind.videoInput, + ); + + when(mediaDevices.enumerateDevices).thenAnswer( + (_) => Future.value([videoDevice]), + ); + + final _ = await CameraPlatform.instance.availableCameras(); + + verify( + () => mediaDevices.getUserMedia( + CameraOptions( + video: VideoConstraints( + deviceId: videoDevice.deviceId, + ), + ).toJson(), + ), + ).called(1); + }); + + testWidgets( + 'calls CameraSettings.getLensDirectionForVideoTrack ' + 'on the first video track of the video input device', (tester) async { + final videoDevice = FakeMediaDeviceInfo( + '1', + 'Camera 1', + MediaDeviceKind.videoInput, + ); + + final videoStream = + FakeMediaStream([MockMediaStreamTrack(), MockMediaStreamTrack()]); + + when( + () => mediaDevices.getUserMedia( + CameraOptions( + video: VideoConstraints(deviceId: videoDevice.deviceId), + ).toJson(), + ), + ).thenAnswer((_) => Future.value(videoStream)); + + when(mediaDevices.enumerateDevices).thenAnswer( + (_) => Future.value([videoDevice]), + ); + + final _ = await CameraPlatform.instance.availableCameras(); + + verify( + () => cameraSettings.getFacingModeForVideoTrack( + videoStream.getVideoTracks().first, + ), + ).called(1); + }); + + testWidgets( + 'returns appropriate camera descriptions ' + 'for multiple media devices', (tester) async { + final firstVideoDevice = FakeMediaDeviceInfo( + '1', + 'Camera 1', + MediaDeviceKind.videoInput, + ); + + final secondVideoDevice = FakeMediaDeviceInfo( + '4', + 'Camera 4', + MediaDeviceKind.videoInput, + ); + + // Create a video stream for the first video device. + final firstVideoStream = + FakeMediaStream([MockMediaStreamTrack(), MockMediaStreamTrack()]); + + // Create a video stream for the second video device. + final secondVideoStream = FakeMediaStream([MockMediaStreamTrack()]); + + // Mock media devices to return two video input devices + // and two audio devices. + when(mediaDevices.enumerateDevices).thenAnswer( + (_) => Future.value([ + firstVideoDevice, + FakeMediaDeviceInfo( + '2', + 'Camera 2', + MediaDeviceKind.audioInput, + ), + FakeMediaDeviceInfo( + '3', + 'Camera 3', + MediaDeviceKind.audioOutput, + ), + secondVideoDevice, + ]), + ); + + // Mock media devices to return the first video stream + // for the first video device. + when( + () => mediaDevices.getUserMedia( + CameraOptions( + video: VideoConstraints(deviceId: firstVideoDevice.deviceId), + ).toJson(), + ), + ).thenAnswer((_) => Future.value(firstVideoStream)); + + // Mock media devices to return the second video stream + // for the second video device. + when( + () => mediaDevices.getUserMedia( + CameraOptions( + video: VideoConstraints(deviceId: secondVideoDevice.deviceId), + ).toJson(), + ), + ).thenAnswer((_) => Future.value(secondVideoStream)); + + // Mock camera settings to return a user facing mode + // for the first video stream. + when( + () => cameraSettings.getFacingModeForVideoTrack( + firstVideoStream.getVideoTracks().first, + ), + ).thenReturn('user'); + + when(() => cameraSettings.mapFacingModeToLensDirection('user')) + .thenReturn(CameraLensDirection.front); + + // Mock camera settings to return an environment facing mode + // for the second video stream. + when( + () => cameraSettings.getFacingModeForVideoTrack( + secondVideoStream.getVideoTracks().first, + ), + ).thenReturn('environment'); + + when(() => cameraSettings.mapFacingModeToLensDirection('environment')) + .thenReturn(CameraLensDirection.back); + + final cameras = await CameraPlatform.instance.availableCameras(); + + // Expect two cameras and ignore two audio devices. + expect( + cameras, + equals([ + CameraDescription( + name: firstVideoDevice.label!, + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ), + CameraDescription( + name: secondVideoDevice.label!, + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ) + ]), + ); + }); + + testWidgets( + 'sets camera metadata ' + 'for the camera description', (tester) async { + final videoDevice = FakeMediaDeviceInfo( + '1', + 'Camera 1', + MediaDeviceKind.videoInput, + ); + + final videoStream = + FakeMediaStream([MockMediaStreamTrack(), MockMediaStreamTrack()]); + + when(mediaDevices.enumerateDevices).thenAnswer( + (_) => Future.value([videoDevice]), + ); + + when( + () => mediaDevices.getUserMedia( + CameraOptions( + video: VideoConstraints(deviceId: videoDevice.deviceId), + ).toJson(), + ), + ).thenAnswer((_) => Future.value(videoStream)); + + when( + () => cameraSettings.getFacingModeForVideoTrack( + videoStream.getVideoTracks().first, + ), + ).thenReturn('left'); + + when(() => cameraSettings.mapFacingModeToLensDirection('left')) + .thenReturn(CameraLensDirection.external); + + final camera = (await CameraPlatform.instance.availableCameras()).first; + + expect( + (CameraPlatform.instance as CameraPlugin).camerasMetadata, + equals({ + camera: CameraMetadata( + deviceId: videoDevice.deviceId!, + facingMode: 'left', + ) + }), + ); + }); }); testWidgets('createCamera throws UnimplementedError', (tester) async { diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart index 03be3f0b3ca6..3702aee8e184 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -4,6 +4,7 @@ import 'dart:html'; +import 'package:camera_web/src/camera_settings.dart'; import 'package:mocktail/mocktail.dart'; class MockWindow extends Mock implements Window {} @@ -12,12 +13,44 @@ class MockNavigator extends Mock implements Navigator {} class MockMediaDevices extends Mock implements MediaDevices {} -/// A fake [DomException] that returns the provided [errorName]. +class MockCameraSettings extends Mock implements CameraSettings {} + +class MockMediaStreamTrack extends Mock implements MediaStreamTrack {} + +/// A fake [MediaStream] that returns the provided [_videoTracks]. +class FakeMediaStream extends Fake implements MediaStream { + FakeMediaStream(this._videoTracks); + + final List _videoTracks; + + @override + List getVideoTracks() => _videoTracks; +} + +/// A fake [MediaDeviceInfo] that returns the provided [_deviceId], [_label] and [_kind]. +class FakeMediaDeviceInfo extends Fake implements MediaDeviceInfo { + FakeMediaDeviceInfo(this._deviceId, this._label, this._kind); + + final String _deviceId; + final String _label; + final String _kind; + + @override + String? get deviceId => _deviceId; + + @override + String? get label => _label; + + @override + String? get kind => _kind; +} + +/// A fake [DomException] that returns the provided error [_name]. class FakeDomException extends Fake implements DomException { - FakeDomException(this.errorName); + FakeDomException(this._name); - final String errorName; + final String _name; @override - String get name => errorName; + String get name => _name; } diff --git a/packages/camera/camera_web/lib/src/camera_settings.dart b/packages/camera/camera_web/lib/src/camera_settings.dart new file mode 100644 index 000000000000..2a1a31ff1cf5 --- /dev/null +++ b/packages/camera/camera_web/lib/src/camera_settings.dart @@ -0,0 +1,108 @@ +// 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 'dart:html' as html; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/foundation.dart'; + +/// A utility to fetch and map camera settings. +class CameraSettings { + // A facing mode constraint name. + static const _facingModeKey = "facingMode"; + + /// The current browser window used to access media devices. + @visibleForTesting + html.Window? window = html.window; + + /// Returns a facing mode of the [videoTrack] + /// (null if the facing mode is not available). + String? getFacingModeForVideoTrack(html.MediaStreamTrack videoTrack) { + final mediaDevices = window?.navigator.mediaDevices; + + // Throw a not supported exception if the current browser window + // does not support any media devices. + if (mediaDevices == null) { + throw CameraException( + CameraErrorCodes.notSupported, + 'The camera is not supported on this device.', + ); + } + + // Check if the camera facing mode is supported by the current browser. + final supportedConstraints = mediaDevices.getSupportedConstraints(); + final facingModeSupported = supportedConstraints[_facingModeKey] ?? false; + + // Return null if the facing mode is not supported. + if (!facingModeSupported) { + return null; + } + + // Extract the facing mode from the video track settings. + // The property may not be available if it's not supported + // by the browser or not available due to context. + // + // MediaTrackSettings: + // https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings + final videoTrackSettings = videoTrack.getSettings(); + final facingMode = videoTrackSettings[_facingModeKey]; + + if (facingMode == null) { + try { + // If the facing mode does not exist in the video track settings, + // check for the facing mode in the video track capabilities. + // + // MediaTrackCapabilities: + // https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackcapabilities + // + // This may throw a not supported error on Firefox. + final videoTrackCapabilities = videoTrack.getCapabilities(); + + // A list of facing mode capabilities as + // the camera may support multiple facing modes. + final facingModeCapabilities = + List.from(videoTrackCapabilities[_facingModeKey] ?? []); + + if (facingModeCapabilities.isNotEmpty) { + final facingModeCapability = facingModeCapabilities.first; + return facingModeCapability; + } else { + // Return null if there are no facing mode capabilities. + return null; + } + } catch (e) { + switch (e.runtimeType.toString()) { + case 'JSNoSuchMethodError': + // Return null if getting capabilities is currently not supported. + return null; + default: + throw CameraException( + CameraErrorCodes.unknown, + 'An unknown error occured when getting the video track capabilities.', + ); + } + } + } + + return facingMode; + } + + /// Maps the given [facingMode] to [CameraLensDirection]. + /// + /// The following values for the facing mode are supported: + /// https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings/facingMode + CameraLensDirection mapFacingModeToLensDirection(String facingMode) { + switch (facingMode) { + case 'user': + return CameraLensDirection.front; + case 'environment': + return CameraLensDirection.back; + case 'left': + case 'right': + default: + return CameraLensDirection.external; + } + } +} diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index fc3be09eec1d..ae9937dd94d3 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -7,6 +7,8 @@ import 'dart:html' as html; import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/types/types.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; @@ -15,18 +17,112 @@ import 'package:flutter_web_plugins/flutter_web_plugins.dart'; /// /// This class implements the `package:camera` functionality for the web. class CameraPlugin extends CameraPlatform { + /// Creates a new instance of [CameraPlugin] + /// with the given [cameraSettings] utility. + CameraPlugin({required CameraSettings cameraSettings}) + : _cameraSettings = cameraSettings; + /// Registers this class as the default instance of [CameraPlatform]. static void registerWith(Registrar registrar) { - CameraPlatform.instance = CameraPlugin(); + CameraPlatform.instance = CameraPlugin( + cameraSettings: CameraSettings(), + ); } - /// The current browser window used to access device cameras. + final CameraSettings _cameraSettings; + + /// Metadata associated with each camera description. + /// Populated in [availableCameras]. + @visibleForTesting + final camerasMetadata = {}; + + /// The current browser window used to access media devices. @visibleForTesting - html.Window? window; + html.Window? window = html.window; @override - Future> availableCameras() { - throw UnimplementedError('availableCameras() is not implemented.'); + Future> availableCameras() async { + final mediaDevices = window?.navigator.mediaDevices; + final cameras = []; + + // Throw a not supported exception if the current browser window + // does not support any media devices. + if (mediaDevices == null) { + throw CameraException( + CameraErrorCodes.notSupported, + 'The camera is not supported on this device.', + ); + } + + // Request available media devices. + final devices = await mediaDevices.enumerateDevices(); + + // Filter video input devices. + final videoInputDevices = devices + .whereType() + .where((device) => device.kind == MediaDeviceKind.videoInput) + + /// The device id property is currently not supported on Internet Explorer: + /// https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId#browser_compatibility + .where((device) => device.deviceId != null); + + // Map video input devices to camera descriptions. + for (final videoInputDevice in videoInputDevices) { + // Get the video stream for the current video input device + // to later use for the available video tracks. + final videoStream = await _getVideoStreamForDevice( + mediaDevices, + videoInputDevice.deviceId!, + ); + + // Get all video tracks in the video stream + // to later extract the lens direction from the first track. + final videoTracks = videoStream.getVideoTracks(); + + if (videoTracks.isNotEmpty) { + // Get the facing mode from the first available video track. + final facingMode = _cameraSettings.getFacingModeForVideoTrack( + videoTracks.first, + ); + + // Get the lens direction based on the facing mode. + // Fallback to the external lens direction + // if the facing mode is not available. + final lensDirection = facingMode != null + ? _cameraSettings.mapFacingModeToLensDirection(facingMode) + : CameraLensDirection.external; + + // Create a camera description. + // + // The name is a camera label which might be empty + // if no permissions to media devices have been granted. + // + // MediaDeviceInfo.label: + // https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/label + // + // Sensor orientation is currently not supported. + final cameraLabel = videoInputDevice.label ?? ''; + final camera = CameraDescription( + name: cameraLabel, + lensDirection: lensDirection, + sensorOrientation: 0, + ); + + final cameraMetadata = CameraMetadata( + deviceId: videoInputDevice.deviceId!, + facingMode: facingMode, + ); + + cameras.add(camera); + + camerasMetadata[camera] = cameraMetadata; + } else { + // Ignore as no video tracks exist in the current video input device. + continue; + } + } + + return cameras; } @override @@ -190,4 +286,17 @@ class CameraPlugin extends CameraPlatform { Future dispose(int cameraId) { throw UnimplementedError('dispose() is not implemented.'); } + + /// Returns a media video stream for the device with the given [deviceId]. + Future _getVideoStreamForDevice( + html.MediaDevices mediaDevices, + String deviceId, + ) { + // Create camera options with the desired device id. + final cameraOptions = CameraOptions( + video: VideoConstraints(deviceId: deviceId), + ); + + return mediaDevices.getUserMedia(cameraOptions.toJson()); + } } diff --git a/packages/camera/camera_web/lib/src/types/camera_metadata.dart b/packages/camera/camera_web/lib/src/types/camera_metadata.dart new file mode 100644 index 000000000000..c9998e58a52c --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/camera_metadata.dart @@ -0,0 +1,37 @@ +// 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 'dart:ui' show hashValues; + +/// Metadata used along the camera description +/// to store additional web-specific camera details. +class CameraMetadata { + /// Creates a new instance of [CameraMetadata] + /// with the given [deviceId] and [facingMode]. + const CameraMetadata({required this.deviceId, required this.facingMode}); + + /// Uniquely identifies the camera device. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId + final String deviceId; + + /// Describes the direction the camera is facing towards. + /// May be `user`, `environment`, `left`, `right` + /// or null if the facing mode is not available. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings/facingMode + final String? facingMode; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CameraMetadata && + other.deviceId == deviceId && + other.facingMode == facingMode; + } + + @override + int get hashCode => hashValues(deviceId.hashCode, facingMode.hashCode); +} diff --git a/packages/camera/camera_web/lib/src/types/media_device_kind.dart b/packages/camera/camera_web/lib/src/types/media_device_kind.dart new file mode 100644 index 000000000000..1f746808df9e --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/media_device_kind.dart @@ -0,0 +1,17 @@ +// 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. + +/// A kind of a media device. +/// +/// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/kind +abstract class MediaDeviceKind { + /// A video input media device kind. + static const videoInput = 'videoinput'; + + /// An audio input media device kind. + static const audioInput = 'audioinput'; + + /// An audio output media device kind. + static const audioOutput = 'audiooutput'; +} diff --git a/packages/camera/camera_web/lib/src/types/types.dart b/packages/camera/camera_web/lib/src/types/types.dart index fc1f931679ff..1a15503715cd 100644 --- a/packages/camera/camera_web/lib/src/types/types.dart +++ b/packages/camera/camera_web/lib/src/types/types.dart @@ -3,4 +3,6 @@ // found in the LICENSE file. export 'camera_error_codes.dart'; +export 'camera_metadata.dart'; export 'camera_options.dart'; +export 'media_device_kind.dart'; diff --git a/packages/camera/camera_web/test/types/camera_metadata_test.dart b/packages/camera/camera_web/test/types/camera_metadata_test.dart new file mode 100644 index 000000000000..c76688f768d7 --- /dev/null +++ b/packages/camera/camera_web/test/types/camera_metadata_test.dart @@ -0,0 +1,25 @@ +// 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:camera_web/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('CameraMetadata', () { + test('supports value equality', () { + expect( + CameraMetadata( + deviceId: 'deviceId', + facingMode: 'environment', + ), + equals( + CameraMetadata( + deviceId: 'deviceId', + facingMode: 'environment', + ), + ), + ); + }); + }); +} From e51722c734bb72da3a3a3dcb588e4c82ba8cbdb1 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 22 Jul 2021 14:41:06 +0200 Subject: [PATCH 019/123] [webview_flutter] Refactored creation of Android WebView for testability. (#4178) --- .../webview_flutter/android/build.gradle | 2 + .../webviewflutter/FlutterWebView.java | 78 +++++++--- ...actory.java => FlutterWebViewFactory.java} | 8 +- .../webviewflutter/WebViewBuilder.java | 141 ++++++++++++++++++ .../webviewflutter/WebViewFlutterPlugin.java | 5 +- .../webviewflutter/FlutterWebViewTest.java | 61 ++++++++ .../webviewflutter/WebViewBuilderTest.java | 99 ++++++++++++ 7 files changed, 369 insertions(+), 25 deletions(-) rename packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/{WebViewFactory.java => FlutterWebViewFactory.java} (71%) create mode 100644 packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java create mode 100644 packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java create mode 100644 packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java diff --git a/packages/webview_flutter/webview_flutter/android/build.gradle b/packages/webview_flutter/webview_flutter/android/build.gradle index 45f769b4bc59..41c702f9fc56 100644 --- a/packages/webview_flutter/webview_flutter/android/build.gradle +++ b/packages/webview_flutter/webview_flutter/android/build.gradle @@ -37,5 +37,7 @@ android { implementation 'androidx.annotation:annotation:1.0.0' implementation 'androidx.webkit:webkit:1.0.0' testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-inline:3.11.1' + testImplementation 'androidx.test:core:1.3.0' } } diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java index ebc7c31987f4..a3b681f27980 100644 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -17,7 +17,7 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import androidx.annotation.NonNull; -import io.flutter.plugin.common.BinaryMessenger; +import androidx.annotation.VisibleForTesting; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -28,6 +28,7 @@ import java.util.Map; public class FlutterWebView implements PlatformView, MethodCallHandler { + private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; private final WebView webView; private final MethodChannel methodChannel; @@ -36,6 +37,7 @@ public class FlutterWebView implements PlatformView, MethodCallHandler { // Verifies that a url opened by `Window.open` has a secure url. private class FlutterWebChromeClient extends WebChromeClient { + @Override public boolean onCreateWindow( final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { @@ -83,8 +85,7 @@ public void onProgressChanged(WebView view, int progress) { @SuppressWarnings("unchecked") FlutterWebView( final Context context, - BinaryMessenger messenger, - int id, + MethodChannel methodChannel, Map params, View containerView) { @@ -93,37 +94,34 @@ public void onProgressChanged(WebView view, int progress) { (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); displayListenerProxy.onPreWebViewInitialization(displayManager); - Boolean usesHybridComposition = (Boolean) params.get("usesHybridComposition"); webView = - (usesHybridComposition) - ? new WebView(context) - : new InputAwareWebView(context, containerView); + createWebView( + new WebViewBuilder(context, containerView), params, new FlutterWebChromeClient()); displayListenerProxy.onPostWebViewInitialization(displayManager); platformThreadHandler = new Handler(context.getMainLooper()); - // Allow local storage. - webView.getSettings().setDomStorageEnabled(true); - webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true); - - // Multi windows is set with FlutterWebChromeClient by default to handle internal bug: b/159892679. - webView.getSettings().setSupportMultipleWindows(true); - webView.setWebChromeClient(new FlutterWebChromeClient()); - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id); - methodChannel.setMethodCallHandler(this); + this.methodChannel = methodChannel; + this.methodChannel.setMethodCallHandler(this); flutterWebViewClient = new FlutterWebViewClient(methodChannel); Map settings = (Map) params.get("settings"); - if (settings != null) applySettings(settings); + if (settings != null) { + applySettings(settings); + } if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) { List names = (List) params.get(JS_CHANNEL_NAMES_FIELD); - if (names != null) registerJavaScriptChannelNames(names); + if (names != null) { + registerJavaScriptChannelNames(names); + } } Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy"); - if (autoMediaPlaybackPolicy != null) updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy); + if (autoMediaPlaybackPolicy != null) { + updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy); + } if (params.containsKey("userAgent")) { String userAgent = (String) params.get("userAgent"); updateUserAgent(userAgent); @@ -134,6 +132,44 @@ public void onProgressChanged(WebView view, int progress) { } } + /** + * Creates a {@link android.webkit.WebView} and configures it according to the supplied + * parameters. + * + *

The {@link WebView} is configured with the following predefined settings: + * + *

    + *
  • always enable the DOM storage API; + *
  • always allow JavaScript to automatically open windows; + *
  • always allow support for multiple windows; + *
  • always use the {@link FlutterWebChromeClient} as web Chrome client. + *
+ * + *

Important: This method is visible for testing purposes only and should + * never be called from outside this class. + * + * @param webViewBuilder a {@link WebViewBuilder} which is responsible for building the {@link + * WebView}. + * @param params creation parameters received over the method channel. + * @param webChromeClient an implementation of WebChromeClient This value may be null. + * @return The new {@link android.webkit.WebView} object. + */ + @VisibleForTesting + static WebView createWebView( + WebViewBuilder webViewBuilder, Map params, WebChromeClient webChromeClient) { + boolean usesHybridComposition = Boolean.TRUE.equals(params.get("usesHybridComposition")); + webViewBuilder + .setUsesHybridComposition(usesHybridComposition) + .setDomStorageEnabled(true) // Always enable DOM storage API. + .setJavaScriptCanOpenWindowsAutomatically( + true) // Always allow automatically opening of windows. + .setSupportMultipleWindows(true) // Always support multiple windows. + .setWebChromeClient( + webChromeClient); // Always use {@link FlutterWebChromeClient} as web Chrome client. + + return webViewBuilder.build(); + } + @Override public View getView() { return webView; @@ -369,7 +405,9 @@ private void applySettings(Map settings) { switch (key) { case "jsMode": Integer mode = (Integer) settings.get(key); - if (mode != null) updateJsMode(mode); + if (mode != null) { + updateJsMode(mode); + } break; case "hasNavigationDelegate": final boolean hasNavigationDelegate = (boolean) settings.get(key); diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java similarity index 71% rename from packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java rename to packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java index 22de668e0126..8fe58104a0fb 100644 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java +++ b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java @@ -7,16 +7,17 @@ import android.content.Context; import android.view.View; import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.platform.PlatformView; import io.flutter.plugin.platform.PlatformViewFactory; import java.util.Map; -public final class WebViewFactory extends PlatformViewFactory { +public final class FlutterWebViewFactory extends PlatformViewFactory { private final BinaryMessenger messenger; private final View containerView; - WebViewFactory(BinaryMessenger messenger, View containerView) { + FlutterWebViewFactory(BinaryMessenger messenger, View containerView) { super(StandardMessageCodec.INSTANCE); this.messenger = messenger; this.containerView = containerView; @@ -26,6 +27,7 @@ public final class WebViewFactory extends PlatformViewFactory { @Override public PlatformView create(Context context, int id, Object args) { Map params = (Map) args; - return new FlutterWebView(context, messenger, id, params, containerView); + MethodChannel methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id); + return new FlutterWebView(context, methodChannel, params, containerView); } } diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java new file mode 100644 index 000000000000..6b8cc51febe8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java @@ -0,0 +1,141 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.content.Context; +import android.view.View; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.webkit.WebView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** Builder used to create {@link android.webkit.WebView} objects. */ +public class WebViewBuilder { + + /** Factory used to create a new {@link android.webkit.WebView} instance. */ + static class WebViewFactory { + + /** + * Creates a new {@link android.webkit.WebView} instance. + * + * @param context an Activity Context to access application assets. This value cannot be null. + * @param usesHybridComposition If {@code false} a {@link InputAwareWebView} instance is + * returned. + * @param containerView must be supplied when the {@code useHybridComposition} parameter is set + * to {@code false}. Used to create an InputConnection on the WebView's dedicated input, or + * IME, thread (see also {@link InputAwareWebView}) + * @return A new instance of the {@link android.webkit.WebView} object. + */ + static WebView create(Context context, boolean usesHybridComposition, View containerView) { + return usesHybridComposition + ? new WebView(context) + : new InputAwareWebView(context, containerView); + } + } + + private final Context context; + private final View containerView; + + private boolean enableDomStorage; + private boolean javaScriptCanOpenWindowsAutomatically; + private boolean supportMultipleWindows; + private boolean usesHybridComposition; + private WebChromeClient webChromeClient; + + /** + * Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link + * WebViewFactory} object. + * + * @param context an Activity Context to access application assets. This value cannot be null. + * @param containerView must be supplied when the {@code useHybridComposition} parameter is set to + * {@code false}. Used to create an InputConnection on the WebView's dedicated input, or IME, + * thread (see also {@link InputAwareWebView}) + */ + WebViewBuilder(@NonNull final Context context, View containerView) { + this.context = context; + this.containerView = containerView; + } + + /** + * Sets whether the DOM storage API is enabled. The default value is {@code false}. + * + * @param flag {@code true} is {@link android.webkit.WebView} should use the DOM storage API. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setDomStorageEnabled(boolean flag) { + this.enableDomStorage = flag; + return this; + } + + /** + * Sets whether JavaScript is allowed to open windows automatically. This applies to the + * JavaScript function {@code window.open()}. The default value is {@code false}. + * + * @param flag {@code true} if JavaScript is allowed to open windows automatically. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setJavaScriptCanOpenWindowsAutomatically(boolean flag) { + this.javaScriptCanOpenWindowsAutomatically = flag; + return this; + } + + /** + * Sets whether the {@link WebView} supports multiple windows. If set to {@code true}, {@link + * WebChromeClient#onCreateWindow} must be implemented by the host application. The default is + * {@code false}. + * + * @param flag {@code true} if multiple windows are supported. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setSupportMultipleWindows(boolean flag) { + this.supportMultipleWindows = flag; + return this; + } + + /** + * Sets whether the hybrid composition should be used. + * + *

If set to {@code true} a standard {@link WebView} is created. If set to {@code false} the + * {@link WebViewBuilder} will create a {@link InputAwareWebView} to workaround issues using the + * {@link WebView} on Android versions below N. + * + * @param flag {@code true} if uses hybrid composition. The default is {@code false}. + * @return This builder. This value cannot be {@code null} + */ + public WebViewBuilder setUsesHybridComposition(boolean flag) { + this.usesHybridComposition = flag; + return this; + } + + /** + * Sets the chrome handler. This is an implementation of WebChromeClient for use in handling + * JavaScript dialogs, favicons, titles, and the progress. This will replace the current handler. + * + * @param webChromeClient an implementation of WebChromeClient This value may be null. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setWebChromeClient(@Nullable WebChromeClient webChromeClient) { + this.webChromeClient = webChromeClient; + return this; + } + + /** + * Build the {@link android.webkit.WebView} using the current settings. + * + * @return The {@link android.webkit.WebView} using the current settings. + */ + public WebView build() { + WebView webView = WebViewFactory.create(context, usesHybridComposition, containerView); + + WebSettings webSettings = webView.getSettings(); + webSettings.setDomStorageEnabled(enableDomStorage); + webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically); + webSettings.setSupportMultipleWindows(supportMultipleWindows); + webView.setWebChromeClient(webChromeClient); + + return webView; + } +} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java index dc329e2273d0..268d35a1e04c 100644 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java @@ -46,7 +46,7 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra .platformViewRegistry() .registerViewFactory( "plugins.flutter.io/webview", - new WebViewFactory(registrar.messenger(), registrar.view())); + new FlutterWebViewFactory(registrar.messenger(), registrar.view())); new FlutterCookieManager(registrar.messenger()); } @@ -56,7 +56,8 @@ public void onAttachedToEngine(FlutterPluginBinding binding) { binding .getPlatformViewRegistry() .registerViewFactory( - "plugins.flutter.io/webview", new WebViewFactory(messenger, /*containerView=*/ null)); + "plugins.flutter.io/webview", + new FlutterWebViewFactory(messenger, /*containerView=*/ null)); flutterCookieManager = new FlutterCookieManager(messenger); } diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java new file mode 100644 index 000000000000..96cbdece387c --- /dev/null +++ b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java @@ -0,0 +1,61 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; + +public class FlutterWebViewTest { + private WebChromeClient mockWebChromeClient; + private WebViewBuilder mockWebViewBuilder; + private WebView mockWebView; + + @Before + public void before() { + mockWebChromeClient = mock(WebChromeClient.class); + mockWebViewBuilder = mock(WebViewBuilder.class); + mockWebView = mock(WebView.class); + + when(mockWebViewBuilder.setDomStorageEnabled(anyBoolean())).thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setJavaScriptCanOpenWindowsAutomatically(anyBoolean())) + .thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setSupportMultipleWindows(anyBoolean())).thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setUsesHybridComposition(anyBoolean())).thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setWebChromeClient(any(WebChromeClient.class))) + .thenReturn(mockWebViewBuilder); + + when(mockWebViewBuilder.build()).thenReturn(mockWebView); + } + + @Test + public void createWebView_should_create_webview_with_default_configuration() { + FlutterWebView.createWebView( + mockWebViewBuilder, createParameterMap(false), mockWebChromeClient); + + verify(mockWebViewBuilder, times(1)).setDomStorageEnabled(true); + verify(mockWebViewBuilder, times(1)).setJavaScriptCanOpenWindowsAutomatically(true); + verify(mockWebViewBuilder, times(1)).setSupportMultipleWindows(true); + verify(mockWebViewBuilder, times(1)).setUsesHybridComposition(false); + verify(mockWebViewBuilder, times(1)).setWebChromeClient(mockWebChromeClient); + } + + private Map createParameterMap(boolean usesHybridComposition) { + Map params = new HashMap<>(); + params.put("usesHybridComposition", usesHybridComposition); + + return params; + } +} diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java new file mode 100644 index 000000000000..48fbce231ed5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java @@ -0,0 +1,99 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.*; + +import android.content.Context; +import android.view.View; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.webkit.WebView; +import io.flutter.plugins.webviewflutter.WebViewBuilder.WebViewFactory; +import java.io.IOException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.MockedStatic.Verification; + +public class WebViewBuilderTest { + private Context mockContext; + private View mockContainerView; + private WebView mockWebView; + private MockedStatic mockedStaticWebViewFactory; + + @Before + public void before() { + mockContext = mock(Context.class); + mockContainerView = mock(View.class); + mockWebView = mock(WebView.class); + mockedStaticWebViewFactory = mockStatic(WebViewFactory.class); + + mockedStaticWebViewFactory + .when( + new Verification() { + @Override + public void apply() { + WebViewFactory.create(mockContext, false, mockContainerView); + } + }) + .thenReturn(mockWebView); + } + + @After + public void after() { + mockedStaticWebViewFactory.close(); + } + + @Test + public void ctor_test() { + WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); + + assertNotNull(builder); + } + + @Test + public void build_should_set_values() throws IOException { + WebSettings mockWebSettings = mock(WebSettings.class); + WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); + + when(mockWebView.getSettings()).thenReturn(mockWebSettings); + + WebViewBuilder builder = + new WebViewBuilder(mockContext, mockContainerView) + .setDomStorageEnabled(true) + .setJavaScriptCanOpenWindowsAutomatically(true) + .setSupportMultipleWindows(true) + .setWebChromeClient(mockWebChromeClient); + + WebView webView = builder.build(); + + assertNotNull(webView); + verify(mockWebSettings).setDomStorageEnabled(true); + verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(true); + verify(mockWebSettings).setSupportMultipleWindows(true); + verify(mockWebView).setWebChromeClient(mockWebChromeClient); + } + + @Test + public void build_should_use_default_values() throws IOException { + WebSettings mockWebSettings = mock(WebSettings.class); + WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); + + when(mockWebView.getSettings()).thenReturn(mockWebSettings); + + WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); + + WebView webView = builder.build(); + + assertNotNull(webView); + verify(mockWebSettings).setDomStorageEnabled(false); + verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(false); + verify(mockWebSettings).setSupportMultipleWindows(false); + verify(mockWebView).setWebChromeClient(null); + } +} From b6d1345a5398a45ae90ba9769ca63789b300ce40 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 22 Jul 2021 11:14:17 -0700 Subject: [PATCH 020/123] [flutter_plugin_tools] Replace xctest and java-test with native-test (#4176) Creates a new `native-test` command that will be used to run native unit and UI/integration tests for all platforms over time. This replaces both `xctest` and `java-test`. For CI we can continue to run each platform separately for clarity, but the combined command makes it easier to use (and remember how to use) for local development, as well as avoiding the need to introduce several new commands for desktop testing as support for that is added to the tool. Fixes https://github.com/flutter/flutter/issues/84392 Fixes https://github.com/flutter/flutter/issues/86489 --- .cirrus.yml | 18 +- script/tool/CHANGELOG.md | 10 +- script/tool/README.md | 24 +- .../src/common/package_looping_command.dart | 4 +- script/tool/lib/src/java_test_command.dart | 78 -- script/tool/lib/src/main.dart | 8 +- script/tool/lib/src/native_test_command.dart | 377 ++++++ script/tool/lib/src/xctest_command.dart | 211 ---- script/tool/test/java_test_command_test.dart | 187 --- .../tool/test/native_test_command_test.dart | 1071 +++++++++++++++++ script/tool/test/xctest_command_test.dart | 705 ----------- 11 files changed, 1491 insertions(+), 1202 deletions(-) delete mode 100644 script/tool/lib/src/java_test_command.dart create mode 100644 script/tool/lib/src/native_test_command.dart delete mode 100644 script/tool/lib/src/xctest_command.dart delete mode 100644 script/tool/test/java_test_command_test.dart create mode 100644 script/tool/test/native_test_command_test.dart delete mode 100644 script/tool/test/xctest_command_test.dart diff --git a/.cirrus.yml b/.cirrus.yml index 96902cfd6d15..edefc19bd21a 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -125,7 +125,7 @@ task: memory: 12G matrix: ### Android tasks ### - - name: build-apks+java-test+firebase-test-lab + - name: build-apks+android-unit+firebase-test-lab env: matrix: PLUGIN_SHARDING: "--shardIndex 0 --shardCount 4" @@ -160,13 +160,15 @@ task: - export CIRRUS_CHANGE_MESSAGE="" - export CIRRUS_COMMIT_MESSAGE="" - ./script/tool_runner.sh build-examples --apk - java_test_script: + native_unit_test_script: # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they # might include non-ASCII characters which makes Gradle crash. # TODO(stuartmorgan): See https://github.com/flutter/flutter/issues/24935 - export CIRRUS_CHANGE_MESSAGE="" - export CIRRUS_COMMIT_MESSAGE="" - - ./script/tool_runner.sh java-test # must come after apk build + # Native integration tests are handled by firebase-test-lab below, so + # only run unit tests. + - ./script/tool_runner.sh native-test --android --no-integration # must come after apk build firebase_test_lab_script: # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they # might include non-ASCII characters which makes Gradle crash. @@ -239,12 +241,12 @@ task: - ./script/tool_runner.sh build-examples --ios xcode_analyze_script: - ./script/tool_runner.sh xcode-analyze --ios - xctest_script: - - ./script/tool_runner.sh xctest --ios --ios-destination "platform=iOS Simulator,name=iPhone 11,OS=latest" + native_test_script: + - ./script/tool_runner.sh native-test --ios --ios-destination "platform=iOS Simulator,name=iPhone 11,OS=latest" drive_script: # `drive-examples` contains integration tests, which changes the UI of the application. # This UI change sometimes affects `xctest`. - # So we run `drive-examples` after `xctest`, changing the order will result ci failure. + # So we run `drive-examples` after `native-test`; changing the order will result ci failure. - ./script/tool_runner.sh drive-examples --ios --exclude $PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS ### macOS desktop tasks ### - name: build_all_plugins_macos @@ -269,7 +271,7 @@ task: - ./script/tool_runner.sh build-examples --macos xcode_analyze_script: - ./script/tool_runner.sh xcode-analyze --macos - xctest_script: - - ./script/tool_runner.sh xctest --macos --exclude $PLUGINS_TO_EXCLUDE_MACOS_XCTESTS + native_test_script: + - ./script/tool_runner.sh native-test --macos --exclude $PLUGINS_TO_EXCLUDE_MACOS_XCTESTS drive_script: - ./script/tool_runner.sh drive-examples --macos diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index d701278ee76f..dc30c05f79c8 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -2,13 +2,21 @@ - Added an `xctest` flag to select specific test targets, to allow running only unit tests or integration tests. -- Split Xcode analysis out of `xctest` and into a new `xcode-analyze` command. +- **Breaking change**: Split Xcode analysis out of `xctest` and into a new + `xcode-analyze` command. - Fixed a bug that caused `firebase-test-lab` to hang if it tried to run more than one plugin's tests in a single run. - **Breaking change**: If `firebase-test-lab` is run on a package that supports Android, but for which no tests are run, it now fails instead of skipping. This matches `drive-examples`, as this command is what is used for driving Android Flutter integration tests on CI. +- **Breaking change**: Replaced `xctest` with a new `native-test` command that + will eventually be able to run native unit and integration tests for all + platforms. + - Adds the ability to disable test types via `--no-unit` or + `--no-integration`. +- **Breaking change**: Replaced `java-test` with Android unit test support for + the new `native-test` command. ## 0.4.1 diff --git a/script/tool/README.md b/script/tool/README.md index 5629dc50646b..1a87f098757b 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -75,14 +75,28 @@ cd dart run ./script/tool/bin/flutter_plugin_tools.dart test --packages plugin_name ``` -### Run XCTests +### Run Dart Integration Tests ```sh cd -# For iOS: -dart run ./script/tool/bin/flutter_plugin_tools.dart xctest --ios --packages plugin_name -# For macOS: -dart run ./script/tool/bin/flutter_plugin_tools.dart xctest --macos --packages plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart build-examples --packages plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart drive-examples --packages plugin_name +``` + +### Run Native Tests + +`native-test` takes one or more platform flags to run tests for. By default it +runs both unit tests and (on platforms that support it) integration tests, but +`--no-unit` or `--no-integration` can be used to run just one type. + +Examples: + +```sh +cd +# Run just unit tests for iOS and Android: +dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --ios --android --no-integration --packages plugin_name +# Run all tests for macOS: +dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --macos --packages plugin_name ``` ### Publish a Release diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 9f4039ec7074..0bcde6d296d3 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -165,9 +165,9 @@ abstract class PackageLoopingCommand extends PluginCommand { final List components = p.posix.split(packageName); // For the common federated plugin pattern of `foo/foo_subpackage`, drop // the first part since it's not useful. - if (components.length == 2 && + if (components.length >= 2 && components[1].startsWith('${components[0]}_')) { - packageName = components[1]; + packageName = p.posix.joinAll(components.sublist(1)); } return packageName; } diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart deleted file mode 100644 index b36d1102f109..000000000000 --- a/script/tool/lib/src/java_test_command.dart +++ /dev/null @@ -1,78 +0,0 @@ -// 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:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; - -/// A command to run the Java tests of Android plugins. -class JavaTestCommand extends PackageLoopingCommand { - /// Creates an instance of the test runner. - JavaTestCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform); - - static const String _gradleWrapper = 'gradlew'; - - @override - final String name = 'java-test'; - - @override - final String description = 'Runs the Java tests of the example apps.\n\n' - 'Building the apks of the example apps is required before executing this' - 'command.'; - - @override - Future runForPackage(Directory package) async { - final Iterable examplesWithTests = getExamplesForPlugin(package) - .where((Directory d) => - isFlutterPackage(d) && - (d - .childDirectory('android') - .childDirectory('app') - .childDirectory('src') - .childDirectory('test') - .existsSync() || - d.parent - .childDirectory('android') - .childDirectory('src') - .childDirectory('test') - .existsSync())); - - if (examplesWithTests.isEmpty) { - return PackageResult.skip('No Java unit tests.'); - } - - final List errors = []; - for (final Directory example in examplesWithTests) { - final String exampleName = getRelativePosixPath(example, from: package); - print('\nRUNNING JAVA TESTS for $exampleName'); - - final Directory androidDirectory = example.childDirectory('android'); - final File gradleFile = androidDirectory.childFile(_gradleWrapper); - if (!gradleFile.existsSync()) { - printError('ERROR: Run "flutter build apk" on $exampleName, or run ' - 'this tool\'s "build-examples --apk" command, ' - 'before executing tests.'); - errors.add('$exampleName has not been built.'); - continue; - } - - final int exitCode = await processRunner.runAndStream( - gradleFile.path, ['testDebugUnitTest', '--info'], - workingDir: androidDirectory); - if (exitCode != 0) { - errors.add('$exampleName tests failed.'); - } - } - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } -} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index ef1a18ab15b2..6001c5df7f0a 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -15,17 +15,16 @@ import 'create_all_plugins_app_command.dart'; import 'drive_examples_command.dart'; import 'firebase_test_lab_command.dart'; import 'format_command.dart'; -import 'java_test_command.dart'; import 'license_check_command.dart'; import 'lint_podspecs_command.dart'; import 'list_command.dart'; +import 'native_test_command.dart'; import 'publish_check_command.dart'; import 'publish_plugin_command.dart'; import 'pubspec_check_command.dart'; import 'test_command.dart'; import 'version_check_command.dart'; import 'xcode_analyze_command.dart'; -import 'xctest_command.dart'; void main(List args) { const FileSystem fileSystem = LocalFileSystem(); @@ -51,17 +50,16 @@ void main(List args) { ..addCommand(DriveExamplesCommand(packagesDir)) ..addCommand(FirebaseTestLabCommand(packagesDir)) ..addCommand(FormatCommand(packagesDir)) - ..addCommand(JavaTestCommand(packagesDir)) ..addCommand(LicenseCheckCommand(packagesDir)) ..addCommand(LintPodspecsCommand(packagesDir)) ..addCommand(ListCommand(packagesDir)) + ..addCommand(NativeTestCommand(packagesDir)) ..addCommand(PublishCheckCommand(packagesDir)) ..addCommand(PublishPluginCommand(packagesDir)) ..addCommand(PubspecCheckCommand(packagesDir)) ..addCommand(TestCommand(packagesDir)) ..addCommand(VersionCheckCommand(packagesDir)) - ..addCommand(XcodeAnalyzeCommand(packagesDir)) - ..addCommand(XCTestCommand(packagesDir)); + ..addCommand(XcodeAnalyzeCommand(packagesDir)); commandRunner.run(args).catchError((Object e) { final ToolExit toolExit = e as ToolExit; diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart new file mode 100644 index 000000000000..73a435d83e1d --- /dev/null +++ b/script/tool/lib/src/native_test_command.dart @@ -0,0 +1,377 @@ +// 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:file/file.dart'; +import 'package:platform/platform.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; +import 'common/xcode.dart'; + +const String _unitTestFlag = 'unit'; +const String _integrationTestFlag = 'integration'; + +const String _iosDestinationFlag = 'ios-destination'; + +const int _exitNoIosSimulators = 3; + +/// The command to run native tests for plugins: +/// - iOS and macOS: XCTests (XCUnitTest and XCUITest) in plugins. +class NativeTestCommand extends PackageLoopingCommand { + /// Creates an instance of the test command. + NativeTestCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : _xcode = Xcode(processRunner: processRunner, log: true), + super(packagesDir, processRunner: processRunner, platform: platform) { + argParser.addOption( + _iosDestinationFlag, + help: 'Specify the destination when running iOS tests.\n' + 'This is passed to the `-destination` argument in the xcodebuild command.\n' + 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT ' + 'for details on how to specify the destination.', + ); + argParser.addFlag(kPlatformAndroid, help: 'Runs Android tests'); + argParser.addFlag(kPlatformIos, help: 'Runs iOS tests'); + argParser.addFlag(kPlatformMacos, help: 'Runs macOS tests'); + + // By default, both unit tests and integration tests are run, but provide + // flags to disable one or the other. + argParser.addFlag(_unitTestFlag, + help: 'Runs native unit tests', defaultsTo: true); + argParser.addFlag(_integrationTestFlag, + help: 'Runs native integration (UI) tests', defaultsTo: true); + } + + static const String _gradleWrapper = 'gradlew'; + + // The device destination flags for iOS tests. + List _iosDestinationFlags = []; + + final Xcode _xcode; + + @override + final String name = 'native-test'; + + @override + final String description = ''' +Runs native unit tests and native integration tests. + +Currently supported platforms: +- Android (unit tests only) +- iOS: requires 'xcrun' to be in your path. +- macOS: requires 'xcrun' to be in your path. + +The example app(s) must be built for all targeted platforms before running +this command. +'''; + + Map _platforms = {}; + + List _requestedPlatforms = []; + + @override + Future initializeRun() async { + _platforms = { + kPlatformAndroid: _PlatformDetails('Android', _testAndroid), + kPlatformIos: _PlatformDetails('iOS', _testIos), + kPlatformMacos: _PlatformDetails('macOS', _testMacOS), + }; + _requestedPlatforms = _platforms.keys + .where((String platform) => getBoolArg(platform)) + .toList(); + _requestedPlatforms.sort(); + + if (_requestedPlatforms.isEmpty) { + printError('At least one platform flag must be provided.'); + throw ToolExit(exitInvalidArguments); + } + + if (!(getBoolArg(_unitTestFlag) || getBoolArg(_integrationTestFlag))) { + printError('At least one test type must be enabled.'); + throw ToolExit(exitInvalidArguments); + } + + if (getBoolArg(kPlatformAndroid) && getBoolArg(_integrationTestFlag)) { + logWarning('This command currently only supports unit tests for Android. ' + 'See https://github.com/flutter/flutter/issues/86490.'); + } + + // iOS-specific run-level state. + if (_requestedPlatforms.contains('ios')) { + String destination = getStringArg(_iosDestinationFlag); + if (destination.isEmpty) { + final String? simulatorId = + await _xcode.findBestAvailableIphoneSimulator(); + if (simulatorId == null) { + printError('Cannot find any available iOS simulators.'); + throw ToolExit(_exitNoIosSimulators); + } + destination = 'id=$simulatorId'; + } + _iosDestinationFlags = [ + '-destination', + destination, + ]; + } + } + + @override + Future runForPackage(Directory package) async { + final List testPlatforms = []; + for (final String platform in _requestedPlatforms) { + if (pluginSupportsPlatform(platform, package, + requiredMode: PlatformSupport.inline)) { + testPlatforms.add(platform); + } else { + print('No implementation for ${_platforms[platform]!.label}.'); + } + } + + if (testPlatforms.isEmpty) { + return PackageResult.skip('Not implemented for target platform(s).'); + } + + final _TestMode mode = _TestMode( + unit: getBoolArg(_unitTestFlag), + integration: getBoolArg(_integrationTestFlag), + ); + + bool ranTests = false; + bool failed = false; + final List failureMessages = []; + for (final String platform in testPlatforms) { + final _PlatformDetails platformInfo = _platforms[platform]!; + print('Running tests for ${platformInfo.label}...'); + print('----------------------------------------'); + final _PlatformResult result = + await platformInfo.testFunction(package, mode); + ranTests |= result.state != RunState.skipped; + if (result.state == RunState.failed) { + failed = true; + + final String? error = result.error; + // Only provide the failing platforms in the failure details if testing + // multiple platforms, otherwise it's just noise. + if (_requestedPlatforms.length > 1) { + failureMessages.add(error != null + ? '${platformInfo.label}: $error' + : platformInfo.label); + } else if (error != null) { + // If there's only one platform, only provide error details in the + // summary if the platform returned a message. + failureMessages.add(error); + } + } + } + + if (!ranTests) { + return PackageResult.skip('No tests found.'); + } + return failed + ? PackageResult.fail(failureMessages) + : PackageResult.success(); + } + + Future<_PlatformResult> _testAndroid(Directory plugin, _TestMode mode) async { + final List examplesWithTests = []; + for (final Directory example in getExamplesForPlugin(plugin)) { + if (!isFlutterPackage(example)) { + continue; + } + if (example + .childDirectory('android') + .childDirectory('app') + .childDirectory('src') + .childDirectory('test') + .existsSync() || + example.parent + .childDirectory('android') + .childDirectory('src') + .childDirectory('test') + .existsSync()) { + examplesWithTests.add(example); + } else { + _printNoExampleTestsMessage(example, 'Android'); + } + } + + if (examplesWithTests.isEmpty) { + return _PlatformResult(RunState.skipped); + } + + bool failed = false; + bool hasMissingBuild = false; + for (final Directory example in examplesWithTests) { + final String exampleName = getPackageDescription(example); + _printRunningExampleTestsMessage(example, 'Android'); + + final Directory androidDirectory = example.childDirectory('android'); + final File gradleFile = androidDirectory.childFile(_gradleWrapper); + if (!gradleFile.existsSync()) { + printError('ERROR: Run "flutter build apk" on $exampleName, or run ' + 'this tool\'s "build-examples --apk" command, ' + 'before executing tests.'); + failed = true; + hasMissingBuild = true; + continue; + } + + final int exitCode = await processRunner.runAndStream( + gradleFile.path, ['testDebugUnitTest', '--info'], + workingDir: androidDirectory); + if (exitCode != 0) { + printError('$exampleName tests failed.'); + failed = true; + } + } + return _PlatformResult(failed ? RunState.failed : RunState.succeeded, + error: + hasMissingBuild ? 'Examples must be built before testing.' : null); + } + + Future<_PlatformResult> _testIos(Directory plugin, _TestMode mode) { + return _runXcodeTests(plugin, 'iOS', mode, + extraFlags: _iosDestinationFlags); + } + + Future<_PlatformResult> _testMacOS(Directory plugin, _TestMode mode) { + return _runXcodeTests(plugin, 'macOS', mode); + } + + /// Runs all applicable tests for [plugin], printing status and returning + /// the test result. + /// + /// The tests targets must be added to the Xcode project of the example app, + /// usually at "example/{ios,macos}/Runner.xcworkspace". + Future<_PlatformResult> _runXcodeTests( + Directory plugin, + String platform, + _TestMode mode, { + List extraFlags = const [], + }) async { + String? testTarget; + if (mode.unitOnly) { + testTarget = 'RunnerTests'; + } else if (mode.integrationOnly) { + testTarget = 'RunnerUITests'; + } + + // Assume skipped until at least one test has run. + RunState overallResult = RunState.skipped; + for (final Directory example in getExamplesForPlugin(plugin)) { + final String exampleName = getPackageDescription(example); + + if (testTarget != null) { + final Directory project = example + .childDirectory(platform.toLowerCase()) + .childDirectory('Runner.xcodeproj'); + final bool? hasTarget = + await _xcode.projectHasTarget(project, testTarget); + if (hasTarget == null) { + printError('Unable to check targets for $exampleName.'); + overallResult = RunState.failed; + continue; + } else if (!hasTarget) { + print('No "$testTarget" target in $exampleName; skipping.'); + continue; + } + } + + _printRunningExampleTestsMessage(example, platform); + final int exitCode = await _xcode.runXcodeBuild( + example, + actions: ['test'], + workspace: '${platform.toLowerCase()}/Runner.xcworkspace', + scheme: 'Runner', + configuration: 'Debug', + extraFlags: [ + if (testTarget != null) '-only-testing:$testTarget', + ...extraFlags, + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + ); + + // The exit code from 'xcodebuild test' when there are no tests. + const int _xcodebuildNoTestExitCode = 66; + switch (exitCode) { + case _xcodebuildNoTestExitCode: + _printNoExampleTestsMessage(example, platform); + continue; + case 0: + printSuccess('Successfully ran $platform xctest for $exampleName'); + // If this is the first test, assume success until something fails. + if (overallResult == RunState.skipped) { + overallResult = RunState.succeeded; + } + break; + default: + // Any failure means a failure overall. + overallResult = RunState.failed; + break; + } + } + return _PlatformResult(overallResult); + } + + /// Prints a standard format message indicating that [platform] tests for + /// [plugin]'s [example] are about to be run. + void _printRunningExampleTestsMessage(Directory example, String platform) { + print('Running $platform tests for ${getPackageDescription(example)}...'); + } + + /// Prints a standard format message indicating that no tests were found for + /// [plugin]'s [example] for [platform]. + void _printNoExampleTestsMessage(Directory example, String platform) { + print('No $platform tests found for ${getPackageDescription(example)}'); + } +} + +// The type for a function that takes a plugin directory and runs its native +// tests for a specific platform. +typedef _TestFunction = Future<_PlatformResult> Function(Directory, _TestMode); + +/// A collection of information related to a specific platform. +class _PlatformDetails { + const _PlatformDetails( + this.label, + this.testFunction, + ); + + /// The name to use in output. + final String label; + + /// The function to call to run tests. + final _TestFunction testFunction; +} + +/// Enabled state for different test types. +class _TestMode { + const _TestMode({required this.unit, required this.integration}); + + final bool unit; + final bool integration; + + bool get integrationOnly => integration && !unit; + bool get unitOnly => unit && !integration; +} + +/// The result of running a single platform's tests. +class _PlatformResult { + _PlatformResult(this.state, {this.error}); + + /// The overall state of the platform's tests. This should be: + /// - failed if any tests failed. + /// - succeeded if at least one test ran, and all tests passed. + /// - skipped if no tests ran. + final RunState state; + + /// An optional error string to include in the summary for this platform. + /// + /// Ignored unless [state] is `failed`. + final String? error; +} diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart deleted file mode 100644 index 44fc3a87d540..000000000000 --- a/script/tool/lib/src/xctest_command.dart +++ /dev/null @@ -1,211 +0,0 @@ -// 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:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/xcode.dart'; - -const String _iosDestinationFlag = 'ios-destination'; -const String _testTargetFlag = 'test-target'; - -// The exit code from 'xcodebuild test' when there are no tests. -const int _xcodebuildNoTestExitCode = 66; - -const int _exitNoSimulators = 3; - -/// The command to run XCTests (XCUnitTest and XCUITest) in plugins. -/// The tests target have to be added to the Xcode project of the example app, -/// usually at "example/{ios,macos}/Runner.xcworkspace". -class XCTestCommand extends PackageLoopingCommand { - /// Creates an instance of the test command. - XCTestCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : _xcode = Xcode(processRunner: processRunner, log: true), - super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addOption( - _iosDestinationFlag, - help: - 'Specify the destination when running the test, used for -destination flag for xcodebuild command.\n' - 'this is passed to the `-destination` argument in xcodebuild command.\n' - 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', - ); - argParser.addOption( - _testTargetFlag, - help: - 'Limits the tests to a specific target (e.g., RunnerTests or RunnerUITests)', - ); - argParser.addFlag(kPlatformIos, help: 'Runs the iOS tests'); - argParser.addFlag(kPlatformMacos, help: 'Runs the macOS tests'); - } - - // The device destination flags for iOS tests. - List _iosDestinationFlags = []; - - final Xcode _xcode; - - @override - final String name = 'xctest'; - - @override - final String description = - 'Runs the xctests in the iOS and/or macOS example apps.\n\n' - 'This command requires "flutter" and "xcrun" to be in your path.'; - - @override - Future initializeRun() async { - final bool shouldTestIos = getBoolArg(kPlatformIos); - final bool shouldTestMacos = getBoolArg(kPlatformMacos); - - if (!(shouldTestIos || shouldTestMacos)) { - printError('At least one platform flag must be provided.'); - throw ToolExit(exitInvalidArguments); - } - - if (shouldTestIos) { - String destination = getStringArg(_iosDestinationFlag); - if (destination.isEmpty) { - final String? simulatorId = - await _xcode.findBestAvailableIphoneSimulator(); - if (simulatorId == null) { - printError('Cannot find any available simulators, tests failed'); - throw ToolExit(_exitNoSimulators); - } - destination = 'id=$simulatorId'; - } - _iosDestinationFlags = [ - '-destination', - destination, - ]; - } - } - - @override - Future runForPackage(Directory package) async { - final bool testIos = getBoolArg(kPlatformIos) && - pluginSupportsPlatform(kPlatformIos, package, - requiredMode: PlatformSupport.inline); - final bool testMacos = getBoolArg(kPlatformMacos) && - pluginSupportsPlatform(kPlatformMacos, package, - requiredMode: PlatformSupport.inline); - - final bool multiplePlatformsRequested = - getBoolArg(kPlatformIos) && getBoolArg(kPlatformMacos); - if (!(testIos || testMacos)) { - String description; - if (multiplePlatformsRequested) { - description = 'Neither iOS nor macOS is'; - } else if (getBoolArg(kPlatformIos)) { - description = 'iOS is not'; - } else { - description = 'macOS is not'; - } - return PackageResult.skip( - '$description implemented by this plugin package.'); - } - - if (multiplePlatformsRequested && (!testIos || !testMacos)) { - print('Only running for ${testIos ? 'iOS' : 'macOS'}\n'); - } - - final List failures = []; - bool ranTests = false; - if (testIos) { - final RunState result = await _testPlugin(package, 'iOS', - extraXcrunFlags: _iosDestinationFlags); - ranTests |= result != RunState.skipped; - if (result == RunState.failed) { - failures.add('iOS'); - } - } - if (testMacos) { - final RunState result = await _testPlugin(package, 'macOS'); - ranTests |= result != RunState.skipped; - if (result == RunState.failed) { - failures.add('macOS'); - } - } - - if (!ranTests) { - return PackageResult.skip('No tests found.'); - } - // Only provide the failing platform in the failure details if testing - // multiple platforms, otherwise it's just noise. - return failures.isEmpty - ? PackageResult.success() - : PackageResult.fail( - multiplePlatformsRequested ? failures : []); - } - - /// Runs all applicable tests for [plugin], printing status and returning - /// the test result. - Future _testPlugin( - Directory plugin, - String platform, { - List extraXcrunFlags = const [], - }) async { - final String testTarget = getStringArg(_testTargetFlag); - - // Assume skipped until at least one test has run. - RunState overallResult = RunState.skipped; - for (final Directory example in getExamplesForPlugin(plugin)) { - final String examplePath = - getRelativePosixPath(example, from: plugin.parent); - - if (testTarget.isNotEmpty) { - final Directory project = example - .childDirectory(platform.toLowerCase()) - .childDirectory('Runner.xcodeproj'); - final bool? hasTarget = - await _xcode.projectHasTarget(project, testTarget); - if (hasTarget == null) { - printError('Unable to check targets for $examplePath.'); - overallResult = RunState.failed; - continue; - } else if (!hasTarget) { - print('No "$testTarget" target in $examplePath; skipping.'); - continue; - } - } - - print('Running $platform tests for $examplePath...'); - final int exitCode = await _xcode.runXcodeBuild( - example, - actions: ['test'], - workspace: '${platform.toLowerCase()}/Runner.xcworkspace', - scheme: 'Runner', - configuration: 'Debug', - extraFlags: [ - if (testTarget.isNotEmpty) '-only-testing:$testTarget', - ...extraXcrunFlags, - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - ); - - switch (exitCode) { - case _xcodebuildNoTestExitCode: - print('No tests found for $examplePath'); - continue; - case 0: - printSuccess('Successfully ran $platform xctest for $examplePath'); - // If this is the first test, assume success until something fails. - if (overallResult == RunState.skipped) { - overallResult = RunState.succeeded; - } - break; - default: - // Any failure means a failure overall. - overallResult = RunState.failed; - break; - } - } - return overallResult; - } -} diff --git a/script/tool/test/java_test_command_test.dart b/script/tool/test/java_test_command_test.dart deleted file mode 100644 index 13e0e7fc0f40..000000000000 --- a/script/tool/test/java_test_command_test.dart +++ /dev/null @@ -1,187 +0,0 @@ -// 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 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:flutter_plugin_tools/src/java_test_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('$JavaTestCommand', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final JavaTestCommand command = JavaTestCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = - CommandRunner('java_test_test', 'Test for $JavaTestCommand'); - runner.addCommand(command); - }); - - test('Should run Java tests in Android implementation folder', () async { - final Directory plugin = createFakePlugin( - 'plugin1', - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline - }, - extraFiles: [ - 'example/android/gradlew', - 'android/src/test/example_test.java', - ], - ); - - await runCapturingPrint(runner, ['java-test']); - - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest', '--info'], - androidFolder.path, - ), - ]), - ); - }); - - test('Should run Java tests in example folder', () async { - final Directory plugin = createFakePlugin( - 'plugin1', - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/test/example_test.java', - ], - ); - - await runCapturingPrint(runner, ['java-test']); - - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest', '--info'], - androidFolder.path, - ), - ]), - ); - }); - - test('fails when the app needs to be built', () async { - createFakePlugin( - 'plugin1', - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline - }, - extraFiles: [ - 'example/android/app/src/test/example_test.java', - ], - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['java-test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('ERROR: Run "flutter build apk" on example'), - contains('plugin1:\n' - ' example has not been built.') - ]), - ); - }); - - test('fails when a test fails', () async { - final Directory pluginDir = createFakePlugin( - 'plugin1', - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/test/example_test.java', - ], - ); - - final String gradlewPath = pluginDir - .childDirectory('example') - .childDirectory('android') - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.failing() - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['java-test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('plugin1:\n' - ' example tests failed.') - ]), - ); - }); - - test('Skips when running no tests', () async { - createFakePlugin( - 'plugin1', - packagesDir, - ); - - final List output = - await runCapturingPrint(runner, ['java-test']); - - expect( - output, - containsAllInOrder( - [contains('SKIPPING: No Java unit tests.')]), - ); - }); - }); -} diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart new file mode 100644 index 000000000000..ca28a6cff0e7 --- /dev/null +++ b/script/tool/test/native_test_command_test.dart @@ -0,0 +1,1071 @@ +// 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 'dart:convert'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/native_test_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +final Map _kDeviceListMap = { + 'runtimes': >[ + { + 'bundlePath': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', + 'buildversion': '17L255', + 'runtimeRoot': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4', + 'version': '13.4', + 'isAvailable': true, + 'name': 'iOS 13.4' + }, + ], + 'devices': { + 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus', + 'state': 'Shutdown', + 'name': 'iPhone 8 Plus' + } + ] + } +}; + +// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of +// doing all the process mocking and validation. +void main() { + const String _kDestination = '--ios-destination'; + + group('test native_test_command', () { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(isMacOS: true); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final NativeTestCommand command = NativeTestCommand(packagesDir, + processRunner: processRunner, platform: mockPlatform); + + runner = CommandRunner( + 'native_test_command', 'Test for native_test_command'); + runner.addCommand(command); + }); + + test('fails if no platforms are provided', () async { + Error? commandError; + final List output = await runCapturingPrint( + runner, ['native-test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('At least one platform flag must be provided.'), + ]), + ); + }); + + test('fails if all test types are disabled', () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--no-unit', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('At least one test type must be enabled.'), + ]), + ); + }); + + test('reports skips with no tests', () async { + final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + // Exit code 66 from testing indicates no tests. + final MockProcess noTestsProcessResult = MockProcess(); + noTestsProcessResult.exitCodeCompleter.complete(66); + processRunner.mockProcessesForExecutable['xcrun'] = [ + noTestsProcessResult, + ]; + final List output = + await runCapturingPrint(runner, ['native-test', '--macos']); + + expect(output, contains(contains('No tests found.'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + group('iOS', () { + test('skip if iOS is not supported', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final List output = await runCapturingPrint(runner, + ['native-test', '--ios', _kDestination, 'foo_destination']); + expect( + output, + containsAllInOrder([ + contains('No implementation for iOS.'), + contains('SKIPPING: Not implemented for target platform(s).'), + ])); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if iOS is implemented in a federated package', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.federated + }); + + final List output = await runCapturingPrint(runner, + ['native-test', '--ios', _kDestination, 'foo_destination']); + expect( + output, + containsAllInOrder([ + contains('No implementation for iOS.'), + contains('SKIPPING: Not implemented for target platform(s).'), + ])); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('running with correct destination', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--ios', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Successfully ran iOS xctest for plugin/example') + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'foo_destination', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('Not specifying --ios-destination assigns an available simulator', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = jsonEncode(_kDeviceListMap); + await runCapturingPrint(runner, ['native-test', '--ios']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall( + 'xcrun', + [ + 'simctl', + 'list', + 'devices', + 'runtimes', + 'available', + '--json', + ], + null), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + }); + + group('macOS', () { + test('skip if macOS is not supported', () async { + createFakePlugin('plugin', packagesDir); + + final List output = + await runCapturingPrint(runner, ['native-test', '--macos']); + + expect( + output, + containsAllInOrder([ + contains('No implementation for macOS.'), + contains('SKIPPING: Not implemented for target platform(s).'), + ])); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if macOS is implemented in a federated package', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.federated, + }); + + final List output = + await runCapturingPrint(runner, ['native-test', '--macos']); + + expect( + output, + containsAllInOrder([ + contains('No implementation for macOS.'), + contains('SKIPPING: Not implemented for target platform(s).'), + ])); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('runs for macOS plugin', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + ]); + + expect( + output, + contains( + contains('Successfully ran macOS xctest for plugin/example'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + }); + + group('Android', () { + test('runs Java tests in Android implementation folder', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'android/src/test/example_test.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest', '--info'], + androidFolder.path, + ), + ]), + ); + }); + + test('runs Java tests in example folder', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest', '--info'], + androidFolder.path, + ), + ]), + ); + }); + + test('fails when the app needs to be built', () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/app/src/test/example_test.java', + ], + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['native-test', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('ERROR: Run "flutter build apk" on plugin/example'), + contains('plugin:\n' + ' Examples must be built before testing.') + ]), + ); + }); + + test('fails when a test fails', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + ], + ); + + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['native-test', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('plugin/example tests failed.'), + contains('The following packages had errors:'), + contains('plugin') + ]), + ); + }); + + test('skips if Android is not supported', () async { + createFakePlugin( + 'plugin', + packagesDir, + ); + + final List output = await runCapturingPrint( + runner, ['native-test', '--android']); + + expect( + output, + containsAllInOrder([ + contains('No implementation for Android.'), + contains('SKIPPING: Not implemented for target platform(s).'), + ]), + ); + }); + + test('skips when running no tests', () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + ); + + final List output = await runCapturingPrint( + runner, ['native-test', '--android']); + + expect( + output, + containsAllInOrder([ + contains('No Android tests found for plugin/example'), + contains('SKIPPING: No tests found.'), + ]), + ); + }); + }); + + // Tests behaviors of implementation that is shared between iOS and macOS. + group('iOS/macOS', () { + test('fails if xcrun fails', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = + await runCapturingPrint(runner, ['native-test', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin'), + ]), + ); + }); + + test('honors unit-only', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = + '{"project":{"targets":["RunnerTests", "RunnerUITests"]}}'; + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--no-integration', + ]); + + expect( + output, + contains( + contains('Successfully ran macOS xctest for plugin/example'))); + + // --no-integration should translate to '-only-testing:RunnerTests'. + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-only-testing:RunnerTests', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('honors integration-only', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = + '{"project":{"targets":["RunnerTests", "RunnerUITests"]}}'; + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--no-unit', + ]); + + expect( + output, + contains( + contains('Successfully ran macOS xctest for plugin/example'))); + + // --no-unit should translate to '-only-testing:RunnerUITests'. + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-only-testing:RunnerUITests', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('skips when the requested target is not present', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.succeeding(); + // Simulate a project with unit tests but no integration tests... + processRunner.resultStdout = '{"project":{"targets":["RunnerTests"]}}'; + // ... then try to run only integration tests. + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--no-unit', + ]); + + expect( + output, + containsAllInOrder([ + contains( + 'No "RunnerUITests" target in plugin/example; skipping.'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ])); + }); + + test('fails if unable to check for requested target', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.failing(); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to check targets for plugin/example.'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ])); + }); + }); + + group('multiplatform', () { + test('runs all platfroms when supported', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/android/gradlew', + 'android/src/test/example_test.java', + ], + platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + kPlatformMacos: PlatformSupport.inline, + }, + ); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + final Directory androidFolder = + pluginExampleDirectory.childDirectory('android'); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--android', + '--ios', + '--macos', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAll([ + contains('Running Android tests for plugin/example'), + contains('Successfully ran iOS xctest for plugin/example'), + contains('Successfully ran macOS xctest for plugin/example'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest', '--info'], + androidFolder.path), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'foo_destination', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('runs only macOS for a macOS plugin', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--ios', + '--macos', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('No implementation for iOS.'), + contains('Successfully ran macOS xctest for plugin/example'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('runs only iOS for a iOS plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--ios', + '--macos', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('No implementation for macOS.'), + contains('Successfully ran iOS xctest for plugin/example') + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'foo_destination', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('skips when nothing is supported', () async { + createFakePlugin('plugin', packagesDir); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--android', + '--ios', + '--macos', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('No implementation for Android.'), + contains('No implementation for iOS.'), + contains('No implementation for macOS.'), + contains('SKIPPING: Not implemented for target platform(s).'), + ])); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('failing one platform does not stop the tests', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + ], + ); + + // Simulate failing Android, but not iOS. + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--android', + '--ios', + '--ios-destination', + 'foo_destination', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('Running tests for Android...'), + contains('plugin/example tests failed.'), + contains('Running tests for iOS...'), + contains('Successfully ran iOS xctest for plugin/example'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' Android') + ]), + ); + }); + + test('failing multiple platforms reports multiple failures', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline, + kPlatformIos: PlatformSupport.inline, + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + ], + ); + + // Simulate failing Android. + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess.failing() + ]; + // Simulate failing Android. + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--android', + '--ios', + '--ios-destination', + 'foo_destination', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('Running tests for Android...'), + contains('Running tests for iOS...'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' Android\n' + ' iOS') + ]), + ); + }); + }); + }); +} diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart deleted file mode 100644 index 324dea0e71ef..000000000000 --- a/script/tool/test/xctest_command_test.dart +++ /dev/null @@ -1,705 +0,0 @@ -// 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 'dart:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:flutter_plugin_tools/src/xctest_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -final Map _kDeviceListMap = { - 'runtimes': >[ - { - 'bundlePath': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', - 'buildversion': '17L255', - 'runtimeRoot': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4', - 'version': '13.4', - 'isAvailable': true, - 'name': 'iOS 13.4' - }, - ], - 'devices': { - 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ - { - 'dataPath': - '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', - 'logPath': - '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'isAvailable': true, - 'deviceTypeIdentifier': - 'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus', - 'state': 'Shutdown', - 'name': 'iPhone 8 Plus' - } - ] - } -}; - -// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of -// doing all the process mocking and validation. -void main() { - const String _kDestination = '--ios-destination'; - - group('test xctest_command', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(isMacOS: true); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final XCTestCommand command = XCTestCommand(packagesDir, - processRunner: processRunner, platform: mockPlatform); - - runner = CommandRunner('xctest_command', 'Test for xctest_command'); - runner.addCommand(command); - }); - - test('Fails if no platforms are provided', () async { - Error? commandError; - final List output = await runCapturingPrint( - runner, ['xctest'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('At least one platform flag must be provided'), - ]), - ); - }); - - test('allows target filtering', () async { - final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); - - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = '{"project":{"targets":["RunnerTests"]}}'; - - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--macos', - '--test-target=RunnerTests', - ]); - - expect( - output, - contains( - contains('Successfully ran macOS xctest for plugin/example'))); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - pluginExampleDirectory - .childDirectory('macos') - .childDirectory('Runner.xcodeproj') - .path, - ], - null), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-only-testing:RunnerTests', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('skips when the requested target is not present', () async { - final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); - - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = '{"project":{"targets":["Runner"]}}'; - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--macos', - '--test-target=RunnerTests', - ]); - - expect( - output, - containsAllInOrder([ - contains('No "RunnerTests" target in plugin/example; skipping.'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - pluginExampleDirectory - .childDirectory('macos') - .childDirectory('Runner.xcodeproj') - .path, - ], - null), - ])); - }); - - test('fails if unable to check for requested target', () async { - final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); - - processRunner.processToReturn = MockProcess.failing(); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--macos', - '--test-target=RunnerTests', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to check targets for plugin/example.'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - pluginExampleDirectory - .childDirectory('macos') - .childDirectory('Runner.xcodeproj') - .path, - ], - null), - ])); - }); - - test('reports skips with no tests', () async { - final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); - - // Exit code 66 from testing indicates no tests. - final MockProcess noTestsProcessResult = MockProcess(); - noTestsProcessResult.exitCodeCompleter.complete(66); - processRunner.mockProcessesForExecutable['xcrun'] = [ - noTestsProcessResult, - ]; - final List output = - await runCapturingPrint(runner, ['xctest', '--macos']); - - expect(output, contains(contains('No tests found.'))); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - group('iOS', () { - test('skip if iOS is not supported', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - final List output = await runCapturingPrint(runner, - ['xctest', '--ios', _kDestination, 'foo_destination']); - expect( - output, - contains( - contains('iOS is not implemented by this plugin package.'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skip if iOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.federated - }); - - final List output = await runCapturingPrint(runner, - ['xctest', '--ios', _kDestination, 'foo_destination']); - expect( - output, - contains( - contains('iOS is not implemented by this plugin package.'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('running with correct destination', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline - }); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--ios', - _kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Successfully ran iOS xctest for plugin/example') - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'foo_destination', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('Not specifying --ios-destination assigns an available simulator', - () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline - }); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = jsonEncode(_kDeviceListMap); - await runCapturingPrint(runner, ['xctest', '--ios']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - const ProcessCall( - 'xcrun', - [ - 'simctl', - 'list', - 'devices', - 'runtimes', - 'available', - '--json', - ], - null), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline - }); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'xctest', - '--ios', - _kDestination, - 'foo_destination', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin'), - ])); - }); - }); - - group('macOS', () { - test('skip if macOS is not supported', () async { - createFakePlugin('plugin', packagesDir); - - final List output = - await runCapturingPrint(runner, ['xctest', '--macos']); - expect( - output, - contains( - contains('macOS is not implemented by this plugin package.'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skip if macOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.federated, - }); - - final List output = - await runCapturingPrint(runner, ['xctest', '--macos']); - expect( - output, - contains( - contains('macOS is not implemented by this plugin package.'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('runs for macOS plugin', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); - - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--macos', - ]); - - expect( - output, - contains( - contains('Successfully ran macOS xctest for plugin/example'))); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['xctest', '--macos'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin'), - ]), - ); - }); - }); - - group('combined', () { - test('runs both iOS and macOS when supported', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, - }); - - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); - - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--ios', - '--macos', - _kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAll([ - contains('Successfully ran iOS xctest for plugin/example'), - contains('Successfully ran macOS xctest for plugin/example'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'foo_destination', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('runs only macOS for a macOS plugin', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); - - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); - - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--ios', - '--macos', - _kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('Only running for macOS'), - contains('Successfully ran macOS xctest for plugin/example'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('runs only iOS for a iOS plugin', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline - }); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--ios', - '--macos', - _kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('Only running for iOS'), - contains('Successfully ran iOS xctest for plugin/example') - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'foo_destination', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('skips when neither are supported', () async { - createFakePlugin('plugin', packagesDir); - - final List output = await runCapturingPrint(runner, [ - 'xctest', - '--ios', - '--macos', - _kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains( - 'SKIPPING: Neither iOS nor macOS is implemented by this plugin package.'), - ])); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - }); - }); -} From 758c55e42064875ddfc6a89373af07db26c7b733 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 22 Jul 2021 14:26:44 -0700 Subject: [PATCH 021/123] [flutter_plugin_tools] Support YAML exception lists (#4183) Currently the tool accepts `--custom-analysis` to allow a list of packages for which custom `analysis_options.yaml` are allowed, and `--exclude` to exclude a set of packages when running a command against all, or all changed, packages. This results in these exception lists being embedded into CI configuration files (e.g., .cirrus.yaml) or scripts, which makes them harder to maintain, and harder to re-use in other contexts (local runs, new CI systems). This adds support for both flags to accept paths to YAML files that contain the lists, so that they can be maintained separately, and with inline comments about the reasons things are on the lists. Also updates the CI to use this new support, eliminating those lists from `.cirrus.yaml` and `tool_runner.sh` Fixes https://github.com/flutter/flutter/issues/86799 --- .cirrus.yml | 36 ++---------- packages/e2e/analysis_options.yaml | 1 - script/configs/README.md | 8 +++ script/configs/custom_analysis.yaml | 41 ++++++++++++++ .../configs/exclude_integration_android.yaml | 22 ++++++++ script/configs/exclude_integration_ios.yaml | 6 ++ script/configs/exclude_integration_web.yaml | 4 ++ script/configs/exclude_native_macos.yaml | 3 + script/tool/CHANGELOG.md | 4 ++ script/tool/lib/src/analyze_command.dart | 21 ++++++- .../tool/lib/src/common/plugin_command.dart | 17 +++++- script/tool/test/analyze_command_test.dart | 19 +++++++ .../tool/test/common/plugin_command_test.dart | 13 +++++ script/tool_runner.sh | 55 +------------------ 14 files changed, 161 insertions(+), 89 deletions(-) delete mode 100644 packages/e2e/analysis_options.yaml create mode 100644 script/configs/README.md create mode 100644 script/configs/custom_analysis.yaml create mode 100644 script/configs/exclude_integration_android.yaml create mode 100644 script/configs/exclude_integration_ios.yaml create mode 100644 script/configs/exclude_integration_web.yaml create mode 100644 script/configs/exclude_native_macos.yaml diff --git a/.cirrus.yml b/.cirrus.yml index edefc19bd21a..54c4c3799ec3 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -72,7 +72,7 @@ task: - cd script/tool - dart analyze --fatal-infos script: - - ./script/tool_runner.sh analyze + - ./script/tool_runner.sh analyze --custom-analysis=script/configs/custom_analysis.yaml ### Android tasks ### - name: build_all_plugins_apk env: @@ -137,22 +137,6 @@ task: CHANNEL: "stable" MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[07586610af1fdfc894e5969f70ef2458341b9b7e9c3b7c4225a663b4a48732b7208a4d91c3b7d45305a6b55fa2a37fc4] - # Currently missing harness files (https://github.com/flutter/flutter/issues/86749): - # camera/camera - # google_sign_in/google_sign_in - # in_app_purchase/in_app_purchase - # in_app_purchase_android - # quick_actions - # shared_preferences/shared_preferences - # url_launcher/url_launcher - # video_player/video_player - # webview_flutter - # Deprecated; no plan to backfill the missing files: - # android_intent,connectivity/connectivity,device_info/device_info,sensors,share,wifi_info_flutter/wifi_info_flutter - # No integration tests to run: - # image_picker/image_picker - Native UI is the critical functionality - # espresso - No Dart code, so no integration tests - PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS: "camera/camera,google_sign_in/google_sign_in,in_app_purchase/in_app_purchase,in_app_purchase_android,quick_actions,shared_preferences/shared_preferences,url_launcher/url_launcher,video_player/video_player,webview_flutter,android_intent,connectivity/connectivity,device_info/device_info,sensors,share,wifi_info_flutter/wifi_info_flutter,image_picker/image_picker,espresso" build_script: # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they # might include non-ASCII characters which makes Gradle crash. @@ -177,16 +161,13 @@ task: - export CIRRUS_COMMIT_MESSAGE="" - if [[ -n "$GCLOUD_FIREBASE_TESTLAB_KEY" ]]; then - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json - - ./script/tool_runner.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26 --exclude $PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS + - ./script/tool_runner.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26 --exclude=script/configs/exclude_integration_android.yaml - else - echo "This user does not have permission to run Firebase Test Lab tests." - fi ### Web tasks ### - name: build-web+drive-examples env: - # Currently missing; see https://github.com/flutter/flutter/issues/81982 - # and https://github.com/flutter/flutter/issues/82211 - PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS: "file_selector,shared_preferences_web" matrix: CHANNEL: "master" CHANNEL: "stable" @@ -199,7 +180,7 @@ task: build_script: - ./script/tool_runner.sh build-examples --web drive_script: - - ./script/tool_runner.sh drive-examples --web --exclude $PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS + - ./script/tool_runner.sh drive-examples --web --exclude=script/configs/exclude_integration_web.yaml # macOS tasks. task: @@ -221,10 +202,6 @@ task: - name: build-ipas+drive-examples env: PATH: $PATH:/usr/local/bin - # in_app_purchase_ios is currently missing tests; see https://github.com/flutter/flutter/issues/81695 - # ios_platform_images is currently missing tests; see https://github.com/flutter/flutter/issues/82208 - # sensor hangs on CI. - PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS: "in_app_purchase_ios,ios_platform_images,sensors" matrix: PLUGIN_SHARDING: "--shardIndex 0 --shardCount 4" PLUGIN_SHARDING: "--shardIndex 1 --shardCount 4" @@ -247,7 +224,7 @@ task: # `drive-examples` contains integration tests, which changes the UI of the application. # This UI change sometimes affects `xctest`. # So we run `drive-examples` after `native-test`; changing the order will result ci failure. - - ./script/tool_runner.sh drive-examples --ios --exclude $PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS + - ./script/tool_runner.sh drive-examples --ios --exclude=script/configs/exclude_integration_ios.yaml ### macOS desktop tasks ### - name: build_all_plugins_macos env: @@ -259,9 +236,6 @@ task: - ./script/build_all_plugins_app.sh macos - name: build-macos+drive-examples env: - # conncectivity_macos is deprecated, so is not getting unit test backfill. - # package_info is deprecated, so is not getting unit test backfill. - PLUGINS_TO_EXCLUDE_MACOS_XCTESTS: "connectivity_macos,package_info" matrix: CHANNEL: "master" CHANNEL: "stable" @@ -272,6 +246,6 @@ task: xcode_analyze_script: - ./script/tool_runner.sh xcode-analyze --macos native_test_script: - - ./script/tool_runner.sh native-test --macos --exclude $PLUGINS_TO_EXCLUDE_MACOS_XCTESTS + - ./script/tool_runner.sh native-test --macos --exclude=script/configs/exclude_native_macos.yaml drive_script: - ./script/tool_runner.sh drive-examples --macos diff --git a/packages/e2e/analysis_options.yaml b/packages/e2e/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/e2e/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/script/configs/README.md b/script/configs/README.md new file mode 100644 index 000000000000..96423cf2779b --- /dev/null +++ b/script/configs/README.md @@ -0,0 +1,8 @@ +This folder contains configuration files that are passed to commands in place +of plugin lists. They are primarily used by CI to opt specific packages out of +tests, but can also useful when running multi-plugin tests locally. + +**Any entry added to a file in this directory should include a comment**. +Skipping tests or checks for plugins is usually not something we want to do, +so should the comment should either include an issue link to the issue tracking +removing it or—much more rarely—explaining why it is a permanent exclusion. diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml new file mode 100644 index 000000000000..f6dc8e288b55 --- /dev/null +++ b/script/configs/custom_analysis.yaml @@ -0,0 +1,41 @@ +# Plugins that deliberately use their own analysis_options.yaml. +# +# This only exists to allow incrementally switching to the newer, stricter +# analysis_options.yaml based on flutter/flutter, rather than the original +# rules based on pedantic (now at analysis_options_legacy.yaml). +# +# DO NOT add new entries to the list, unless it is to push the legacy rules +# from a top-level package into more specific packages in order to incrementally +# migrate a federated plugin. +# +# TODO(ecosystem): Remove everything from this list. See: +# https://github.com/flutter/flutter/issues/76229 +- camera +- file_selector +- flutter_plugin_android_lifecycle +- google_maps_flutter +- google_sign_in +- image_picker +- in_app_purchase +- integration_test +- ios_platform_images +- local_auth +- plugin_platform_interface +- quick_actions +- shared_preferences +- url_launcher +- video_player +- webview_flutter + +# These plugins are deprecated in favor of the Community Plus versions, and +# will be removed from the repo once the critical support window has passed, +# so are not worth updating. +- android_alarm_manager +- android_intent +- battery +- connectivity +- device_info +- package_info +- sensors +- share +- wifi_info_flutter diff --git a/script/configs/exclude_integration_android.yaml b/script/configs/exclude_integration_android.yaml new file mode 100644 index 000000000000..9fc31ec2166a --- /dev/null +++ b/script/configs/exclude_integration_android.yaml @@ -0,0 +1,22 @@ +# Currently missing harness files: https://github.com/flutter/flutter/issues/86749) +- camera/camera +- google_sign_in/google_sign_in +- in_app_purchase/in_app_purchase +- in_app_purchase_android +- quick_actions +- shared_preferences/shared_preferences +- url_launcher/url_launcher +- video_player/video_player +- webview_flutter + +# Deprecated; no plan to backfill the missing files +- android_intent +- connectivity/connectivity +- device_info/device_info +- sensors +- share +- wifi_info_flutter/wifi_info_flutter + +# No integration tests to run: +- image_picker/image_picker +- espresso diff --git a/script/configs/exclude_integration_ios.yaml b/script/configs/exclude_integration_ios.yaml new file mode 100644 index 000000000000..e1ae6adf49cf --- /dev/null +++ b/script/configs/exclude_integration_ios.yaml @@ -0,0 +1,6 @@ +# Currently missing: https://github.com/flutter/flutter/issues/81695 +- in_app_purchase_ios +# Currently missing: https://github.com/flutter/flutter/issues/82208 +- ios_platform_images +# Hangs on CI. Deprecated, so there is no plan to fix it. +- sensors diff --git a/script/configs/exclude_integration_web.yaml b/script/configs/exclude_integration_web.yaml new file mode 100644 index 000000000000..99e20831b3c2 --- /dev/null +++ b/script/configs/exclude_integration_web.yaml @@ -0,0 +1,4 @@ +# Currently missing: https://github.com/flutter/flutter/issues/81982 +- shared_preferences_web +# Currently missing: https://github.com/flutter/flutter/issues/82211 +- file_selector diff --git a/script/configs/exclude_native_macos.yaml b/script/configs/exclude_native_macos.yaml new file mode 100644 index 000000000000..8a817a9c0178 --- /dev/null +++ b/script/configs/exclude_native_macos.yaml @@ -0,0 +1,3 @@ +# Deprecated plugins that will not be getting unit test backfill. +- connectivity_macos +- package_info diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index dc30c05f79c8..7d1eac01b760 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,9 @@ ## NEXT +- `--exclude` and `--custom-analysis` now accept paths to YAML files that + contain lists of packages to exclude, in addition to just package names, + so that exclude lists can be maintained separately from scripts and CI + configuration. - Added an `xctest` flag to select specific test targets, to allow running only unit tests or integration tests. - **Breaking change**: Split Xcode analysis out of `xctest` and into a new diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index e56b95d88eb0..4fd15f027f50 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:file/file.dart'; import 'package:platform/platform.dart'; +import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; @@ -23,7 +24,10 @@ class AnalyzeCommand extends PackageLoopingCommand { }) : super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addMultiOption(_customAnalysisFlag, help: - 'Directories (comma separated) that are allowed to have their own analysis options.', + 'Directories (comma separated) that are allowed to have their own ' + 'analysis options.\n\n' + 'Alternately, a list of one or more YAML files that contain a list ' + 'of allowed directories.', defaultsTo: []); argParser.addOption(_analysisSdk, valueHelp: 'dart-sdk', @@ -37,6 +41,8 @@ class AnalyzeCommand extends PackageLoopingCommand { late String _dartBinaryPath; + Set _allowedCustomAnalysisDirectories = const {}; + @override final String name = 'analyze'; @@ -56,7 +62,7 @@ class AnalyzeCommand extends PackageLoopingCommand { continue; } - final bool allowed = (getStringListArg(_customAnalysisFlag)).any( + final bool allowed = _allowedCustomAnalysisDirectories.any( (String directory) => directory.isNotEmpty && path.isWithin( @@ -107,6 +113,17 @@ class AnalyzeCommand extends PackageLoopingCommand { throw ToolExit(_exitPackagesGetFailed); } + _allowedCustomAnalysisDirectories = + getStringListArg(_customAnalysisFlag).expand((String item) { + if (item.endsWith('.yaml')) { + final File file = packagesDir.fileSystem.file(item); + return (loadYaml(file.readAsStringSync()) as YamlList) + .toList() + .cast(); + } + return [item]; + }).toSet(); + // Use the Dart SDK override if one was passed in. final String? dartSdk = argResults![_analysisSdk] as String?; _dartBinaryPath = diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index ecdcb0565d35..7781eee0d961 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -9,6 +9,7 @@ import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; +import 'package:yaml/yaml.dart'; import 'core.dart'; import 'git_version_finder.dart'; @@ -48,7 +49,9 @@ abstract class PluginCommand extends Command { argParser.addMultiOption( _excludeArg, abbr: 'e', - help: 'Exclude packages from this command.', + help: 'A list of packages to exclude from from this command.\n\n' + 'Alternately, a list of one or more YAML files that contain a list ' + 'of packages to exclude.', defaultsTo: [], ); argParser.addFlag(_runOnChangedPackagesArg, @@ -214,8 +217,18 @@ abstract class PluginCommand extends Command { /// of packages in the flutter/packages repository. Stream _getAllPlugins() async* { Set plugins = Set.from(getStringListArg(_packagesArg)); + final Set excludedPlugins = - Set.from(getStringListArg(_excludeArg)); + getStringListArg(_excludeArg).expand((String item) { + if (item.endsWith('.yaml')) { + final File file = packagesDir.fileSystem.file(item); + return (loadYaml(file.readAsStringSync()) as YamlList) + .toList() + .cast(); + } + return [item]; + }).toSet(); + final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); if (plugins.isEmpty && runOnChangedPackages && diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 69a2c4f95523..9dc8b6a3fca5 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -176,6 +176,25 @@ void main() { ])); }); + test('takes an allow config file', () async { + final Directory pluginDir = createFakePlugin('foo', packagesDir, + extraFiles: ['analysis_options.yaml']); + final File allowFile = packagesDir.childFile('custom.yaml'); + allowFile.writeAsStringSync('- foo'); + + await runCapturingPrint( + runner, ['analyze', '--custom-analysis', allowFile.path]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', const ['packages', 'get'], pluginDir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + pluginDir.path), + ])); + }); + // See: https://github.com/flutter/flutter/issues/78994 test('takes an empty allow list', () async { createFakePlugin('foo', packagesDir, diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index fdab9612be3f..7f67acfb2df3 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -172,6 +172,19 @@ void main() { expect(plugins, unorderedEquals([plugin2.path])); }); + test('exclude accepts config files', () async { + createFakePlugin('plugin1', packagesDir); + final File configFile = packagesDir.childFile('exclude.yaml'); + configFile.writeAsStringSync('- plugin1'); + + await runCapturingPrint(runner, [ + 'sample', + '--packages=plugin1', + '--exclude=${configFile.path}' + ]); + expect(plugins, unorderedEquals([])); + }); + group('test run-on-changed-packages', () { test('all plugins should be tested if there are no changes.', () async { final Directory plugin1 = createFakePlugin('plugin1', packagesDir); diff --git a/script/tool_runner.sh b/script/tool_runner.sh index d16e940d5a4d..11a54ce435a4 100755 --- a/script/tool_runner.sh +++ b/script/tool_runner.sh @@ -10,58 +10,7 @@ readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" source "$SCRIPT_DIR/common.sh" -# Plugins that are excluded from this task. -ALL_EXCLUDED=("") - -# Plugins that deliberately use their own analysis_options.yaml. -# -# This list should only be deleted from, never added to. This only exists -# because we adopted stricter analysis rules recently and needed to exclude -# already failing packages to start linting the repo as a whole. -# -# Finding all: `find packages -name analysis_options.yaml | sort | cut -d/ -f2` -# -# TODO(ecosystem): Remove everything from this list. https://github.com/flutter/flutter/issues/76229 -CUSTOM_ANALYSIS_PLUGINS=( - android_alarm_manager - android_intent - battery - camera - connectivity - cross_file - device_info - e2e - espresso - file_selector - flutter_plugin_android_lifecycle - google_maps_flutter - google_sign_in - image_picker - in_app_purchase - integration_test - ios_platform_images - local_auth - package_info - plugin_platform_interface - quick_actions - sensors - share - shared_preferences - url_launcher - video_player - webview_flutter - wifi_info_flutter -) - -# Comma-separated string of the list above -readonly CUSTOM_FLAG=$(IFS=, ; echo "${CUSTOM_ANALYSIS_PLUGINS[*]}") -# Set some default actions if run without arguments. ACTIONS=("$@") -if [[ "${#ACTIONS[@]}" == 0 ]]; then - ACTIONS=("analyze" "--custom-analysis" "$CUSTOM_FLAG" "test" "java-test") -elif [[ "${ACTIONS[0]}" == "analyze" ]]; then - ACTIONS=("${ACTIONS[@]}" "--custom-analysis" "$CUSTOM_FLAG") -fi BRANCH_NAME="${BRANCH_NAME:-"$(git rev-parse --abbrev-ref HEAD)"}" @@ -71,8 +20,8 @@ PLUGIN_SHARDING=($PLUGIN_SHARDING) if [[ "${BRANCH_NAME}" == "master" ]]; then echo "Running for all packages" - (cd "$REPO_DIR" && plugin_tools "${ACTIONS[@]}" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) + (cd "$REPO_DIR" && plugin_tools "${ACTIONS[@]}" ${PLUGIN_SHARDING[@]}) else echo running "${ACTIONS[@]}" - (cd "$REPO_DIR" && plugin_tools "${ACTIONS[@]}" --run-on-changed-packages --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) + (cd "$REPO_DIR" && plugin_tools "${ACTIONS[@]}" --run-on-changed-packages ${PLUGIN_SHARDING[@]}) fi From dd66f34a767d0df715948c751bf3bc8db3574af5 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 23 Jul 2021 01:22:48 +0200 Subject: [PATCH 022/123] [camera_web] Add `createCamera` implementation (#4182) --- .../camera_settings_test.dart | 95 ++++++++++++ .../integration_test/camera_web_test.dart | 144 ++++++++++++++++-- .../camera_web/lib/src/camera_settings.dart | 35 +++++ .../camera/camera_web/lib/src/camera_web.dart | 53 ++++++- .../lib/src/types/camera_error_codes.dart | 3 + 5 files changed, 318 insertions(+), 12 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart index c1c00fe7a337..ddfb86e4ec0a 100644 --- a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:html'; +import 'dart:ui'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera_settings.dart'; @@ -204,6 +205,100 @@ void main() { ); }); }); + + group('mapFacingModeToCameraType', () { + testWidgets( + 'returns user ' + 'when the facing mode is user', (tester) async { + expect( + settings.mapFacingModeToCameraType('user'), + equals(CameraType.user), + ); + }); + + testWidgets( + 'returns environment ' + 'when the facing mode is environment', (tester) async { + expect( + settings.mapFacingModeToCameraType('environment'), + equals(CameraType.environment), + ); + }); + + testWidgets( + 'returns user ' + 'when the facing mode is left', (tester) async { + expect( + settings.mapFacingModeToCameraType('left'), + equals(CameraType.user), + ); + }); + + testWidgets( + 'returns user ' + 'when the facing mode is right', (tester) async { + expect( + settings.mapFacingModeToCameraType('right'), + equals(CameraType.user), + ); + }); + }); + + group('mapResolutionPresetToSize', () { + testWidgets( + 'returns 3840x2160 ' + 'when the resolution preset is max', (tester) async { + expect( + settings.mapResolutionPresetToSize(ResolutionPreset.max), + equals(Size(3840, 2160)), + ); + }); + + testWidgets( + 'returns 3840x2160 ' + 'when the resolution preset is ultraHigh', (tester) async { + expect( + settings.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), + equals(Size(3840, 2160)), + ); + }); + + testWidgets( + 'returns 1920x1080 ' + 'when the resolution preset is veryHigh', (tester) async { + expect( + settings.mapResolutionPresetToSize(ResolutionPreset.veryHigh), + equals(Size(1920, 1080)), + ); + }); + + testWidgets( + 'returns 1280x720 ' + 'when the resolution preset is high', (tester) async { + expect( + settings.mapResolutionPresetToSize(ResolutionPreset.high), + equals(Size(1280, 720)), + ); + }); + + testWidgets( + 'returns 720x480 ' + 'when the resolution preset is medium', (tester) async { + expect( + settings.mapResolutionPresetToSize(ResolutionPreset.medium), + equals(Size(720, 480)), + ); + }); + + testWidgets( + 'returns 320x240 ' + 'when the resolution preset is low', (tester) async { + expect( + settings.mapResolutionPresetToSize(ResolutionPreset.low), + equals(Size(320, 240)), + ); + }); + }); }); } diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 25368daf02f7..eef17ecfdff9 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -3,9 +3,11 @@ // found in the LICENSE file. import 'dart:html'; +import 'dart:ui'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/camera_web.dart'; +import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_settings.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/services.dart'; @@ -296,18 +298,140 @@ void main() { }); }); - testWidgets('createCamera throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.createCamera( - CameraDescription( + group('createCamera', () { + testWidgets( + 'throws CameraException ' + 'with missingMetadata error ' + 'if there is no metadata ' + 'for the given camera description', (tester) async { + expect( + () => CameraPlatform.instance.createCamera( + CameraDescription( + name: 'name', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.ultraHigh, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.missingMetadata, + ), + ), + ); + }); + + group('creates a camera', () { + const ultraHighResolutionSize = Size(3840, 2160); + const maxResolutionSize = Size(3840, 2160); + + late CameraDescription cameraDescription; + late CameraMetadata cameraMetadata; + + setUp(() { + cameraDescription = CameraDescription( name: 'name', - lensDirection: CameraLensDirection.external, + lensDirection: CameraLensDirection.front, sensorOrientation: 0, - ), - ResolutionPreset.medium, - ), - throwsUnimplementedError, - ); + ); + + cameraMetadata = CameraMetadata( + deviceId: 'deviceId', + facingMode: 'user', + ); + + // Add metadata for the camera description. + (CameraPlatform.instance as CameraPlugin) + .camerasMetadata[cameraDescription] = cameraMetadata; + + when( + () => cameraSettings.mapFacingModeToCameraType('user'), + ).thenReturn(CameraType.user); + }); + + testWidgets('with appropriate options', (tester) async { + when( + () => cameraSettings + .mapResolutionPresetToSize(ResolutionPreset.ultraHigh), + ).thenReturn(ultraHighResolutionSize); + + final cameraId = await CameraPlatform.instance.createCamera( + cameraDescription, + ResolutionPreset.ultraHigh, + enableAudio: true, + ); + + expect( + (CameraPlatform.instance as CameraPlugin).cameras[cameraId], + isA() + .having( + (camera) => camera.textureId, + 'textureId', + cameraId, + ) + .having( + (camera) => camera.window, + 'window', + window, + ) + .having( + (camera) => camera.options, + 'options', + CameraOptions( + audio: AudioConstraints(enabled: true), + video: VideoConstraints( + facingMode: FacingModeConstraint(CameraType.user), + width: VideoSizeConstraint( + ideal: ultraHighResolutionSize.width.toInt(), + ), + height: VideoSizeConstraint( + ideal: ultraHighResolutionSize.height.toInt(), + ), + deviceId: cameraMetadata.deviceId, + ), + ), + ), + ); + }); + + testWidgets( + 'with a max resolution preset ' + 'and enabled audio set to false ' + 'when no options are specified', (tester) async { + when( + () => + cameraSettings.mapResolutionPresetToSize(ResolutionPreset.max), + ).thenReturn(maxResolutionSize); + + final cameraId = await CameraPlatform.instance.createCamera( + cameraDescription, + null, + ); + + expect( + (CameraPlatform.instance as CameraPlugin).cameras[cameraId], + isA().having( + (camera) => camera.options, + 'options', + CameraOptions( + audio: AudioConstraints(enabled: false), + video: VideoConstraints( + facingMode: FacingModeConstraint(CameraType.user), + width: VideoSizeConstraint( + ideal: maxResolutionSize.width.toInt(), + ), + height: VideoSizeConstraint( + ideal: maxResolutionSize.height.toInt(), + ), + deviceId: cameraMetadata.deviceId, + ), + ), + ), + ); + }); + }); }); testWidgets('initializeCamera throws UnimplementedError', (tester) async { diff --git a/packages/camera/camera_web/lib/src/camera_settings.dart b/packages/camera/camera_web/lib/src/camera_settings.dart index 2a1a31ff1cf5..7b87840a90f8 100644 --- a/packages/camera/camera_web/lib/src/camera_settings.dart +++ b/packages/camera/camera_web/lib/src/camera_settings.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:html' as html; +import 'dart:ui'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/types/types.dart'; @@ -105,4 +106,38 @@ class CameraSettings { return CameraLensDirection.external; } } + + /// Maps the given [facingMode] to [CameraType]. + /// + /// See [CameraMetadata.facingMode] for more details. + CameraType mapFacingModeToCameraType(String facingMode) { + switch (facingMode) { + case 'user': + return CameraType.user; + case 'environment': + return CameraType.environment; + case 'left': + case 'right': + default: + return CameraType.user; + } + } + + /// Maps the given [resolutionPreset] to [Size]. + Size mapResolutionPresetToSize(ResolutionPreset resolutionPreset) { + switch (resolutionPreset) { + case ResolutionPreset.max: + case ResolutionPreset.ultraHigh: + return Size(3840, 2160); + case ResolutionPreset.veryHigh: + return Size(1920, 1080); + case ResolutionPreset.high: + return Size(1280, 720); + case ResolutionPreset.medium: + return Size(720, 480); + case ResolutionPreset.low: + default: + return Size(320, 240); + } + } } diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index ae9937dd94d3..80ab13d37d13 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -7,6 +7,7 @@ import 'dart:html' as html; import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_settings.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/material.dart'; @@ -31,6 +32,11 @@ class CameraPlugin extends CameraPlatform { final CameraSettings _cameraSettings; + /// The cameras managed by the [CameraPlugin]. + @visibleForTesting + final cameras = {}; + var _textureCounter = 1; + /// Metadata associated with each camera description. /// Populated in [availableCameras]. @visibleForTesting @@ -130,8 +136,51 @@ class CameraPlugin extends CameraPlatform { CameraDescription cameraDescription, ResolutionPreset? resolutionPreset, { bool enableAudio = false, - }) { - throw UnimplementedError('createCamera() is not implemented.'); + }) async { + if (!camerasMetadata.containsKey(cameraDescription)) { + throw CameraException( + CameraErrorCodes.missingMetadata, + 'Missing camera metadata. Make sure to call `availableCameras` before creating a camera.', + ); + } + + final textureId = _textureCounter++; + + final cameraMetadata = camerasMetadata[cameraDescription]!; + + final cameraType = cameraMetadata.facingMode != null + ? _cameraSettings.mapFacingModeToCameraType(cameraMetadata.facingMode!) + : null; + + // Use the highest resolution possible + // if the resolution preset is not specified. + final videoSize = _cameraSettings + .mapResolutionPresetToSize(resolutionPreset ?? ResolutionPreset.max); + + // Create a camera with the given audio and video constraints. + // Sensor orientation is currently not supported. + final camera = Camera( + textureId: textureId, + window: window, + options: CameraOptions( + audio: AudioConstraints(enabled: enableAudio), + video: VideoConstraints( + facingMode: + cameraType != null ? FacingModeConstraint(cameraType) : null, + width: VideoSizeConstraint( + ideal: videoSize.width.toInt(), + ), + height: VideoSizeConstraint( + ideal: videoSize.height.toInt(), + ), + deviceId: cameraMetadata.deviceId, + ), + ), + ); + + cameras[textureId] = camera; + + return textureId; } @override diff --git a/packages/camera/camera_web/lib/src/types/camera_error_codes.dart b/packages/camera/camera_web/lib/src/types/camera_error_codes.dart index f8dc5dfc4e32..afb02ae3aaa9 100644 --- a/packages/camera/camera_web/lib/src/types/camera_error_codes.dart +++ b/packages/camera/camera_web/lib/src/types/camera_error_codes.dart @@ -24,6 +24,9 @@ abstract class CameraErrorCodes { /// to access the media input from an insecure context. static const type = 'cameraType'; + /// The camera metadata is missing. + static const missingMetadata = 'missingMetadata'; + /// An unknown camera error. static const unknown = 'cameraUnknown'; } From e843c035cf63e73a1d543c5b14079729cb473f8d Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Fri, 23 Jul 2021 11:04:24 -0700 Subject: [PATCH 023/123] Remove references to the V1 Android embedding (#4160) --- packages/android_alarm_manager/CHANGELOG.md | 4 ++ packages/android_alarm_manager/README.md | 49 ------------------- .../androidalarmmanager/AlarmService.java | 19 ------- .../AndroidAlarmManagerPlugin.java | 2 - .../FlutterBackgroundExecutor.java | 40 ++++----------- .../PluginRegistrantException.java | 13 ----- .../android/app/src/main/AndroidManifest.xml | 10 ---- .../Application.java | 26 ---------- .../EmbeddingV1Activity.java | 20 -------- packages/android_intent/CHANGELOG.md | 4 ++ .../EmbeddingV1ActivityTest.java | 17 ------- .../android/app/src/main/AndroidManifest.xml | 17 +------ .../EmbeddingV1Activity.java | 18 ------- packages/camera/camera/CHANGELOG.md | 6 ++- .../EmbeddingV1ActivityTest.java | 18 ------- .../android/app/src/main/AndroidManifest.xml | 15 +----- .../cameraexample/EmbeddingV1Activity.java | 26 ---------- packages/camera/camera/pubspec.yaml | 2 +- .../connectivity/connectivity/CHANGELOG.md | 4 ++ .../android/app/src/main/AndroidManifest.xml | 10 +--- .../EmbeddingV1Activity.java | 18 ------- .../EmbeddingV1ActivityTest.java | 18 ------- packages/device_info/device_info/CHANGELOG.md | 4 ++ .../android/app/src/main/AndroidManifest.xml | 13 +---- .../EmbeddingV1Activity.java | 17 ------- .../EmbeddingV1ActivityTest.java | 18 ------- packages/espresso/CHANGELOG.md | 4 ++ .../android/app/src/main/AndroidManifest.xml | 6 --- packages/espresso/pubspec.yaml | 2 +- .../CHANGELOG.md | 11 +++-- .../EmbeddingV1ActivityTest.java | 18 ------- .../android/app/src/main/AndroidManifest.xml | 14 ------ .../EmbeddingV1Activity.java | 22 --------- .../pubspec.yaml | 2 +- .../google_maps_flutter/CHANGELOG.md | 3 +- .../googlemaps/EmbeddingV1ActivityTest.java | 19 ------- .../android/app/src/main/AndroidManifest.xml | 12 +---- .../EmbeddingV1Activity.java | 20 -------- .../google_maps_flutter/pubspec.yaml | 2 +- .../google_sign_in/CHANGELOG.md | 4 ++ .../android/app/src/main/AndroidManifest.xml | 6 --- .../EmbeddingV1Activity.java | 19 ------- .../EmbeddingV1ActivityTest.java | 18 ------- .../google_sign_in/pubspec.yaml | 2 +- .../android/app/src/main/AndroidManifest.xml | 7 --- .../EmbeddingV1Activity.java | 21 -------- .../EmbeddingV1ActivityTest.java | 18 ------- .../in_app_purchase/CHANGELOG.md | 4 ++ .../android/app/src/main/AndroidManifest.xml | 27 +--------- .../EmbeddingV1Activity.java | 24 --------- .../EmbeddingV1ActivityTest.java | 18 ------- .../in_app_purchase/pubspec.yaml | 2 +- .../android/app/src/main/AndroidManifest.xml | 23 --------- .../EmbeddingV1Activity.java | 24 --------- .../EmbeddingV1ActivityTest.java | 18 ------- packages/local_auth/CHANGELOG.md | 4 ++ .../localauth/EmbeddingV1ActivityTest.java | 19 ------- .../android/app/src/main/AndroidManifest.xml | 8 +-- .../localauthexample/EmbeddingV1Activity.java | 25 ---------- packages/local_auth/pubspec.yaml | 2 +- packages/package_info/CHANGELOG.md | 4 ++ .../android/app/src/main/AndroidManifest.xml | 2 +- .../path_provider/path_provider/CHANGELOG.md | 3 +- .../java/EmbeddingV1ActivityTest.java | 19 ------- .../android/app/src/main/AndroidManifest.xml | 8 +-- .../EmbeddingV1Activity.java | 21 -------- .../path_provider/path_provider/pubspec.yaml | 2 +- .../quick_actions/quick_actions/CHANGELOG.md | 4 ++ .../android/app/src/main/AndroidManifest.xml | 32 +++++------- .../EmbeddingV1Activity.java | 18 ------- .../EmbeddingV1ActivityTest.java | 18 ------- .../quick_actions/quick_actions/pubspec.yaml | 2 +- packages/sensors/CHANGELOG.md | 4 ++ .../android/app/src/main/AndroidManifest.xml | 9 +--- .../sensorsexample/EmbeddingV1Activity.java | 20 -------- .../EmbeddingV1ActivityTest.java | 18 ------- packages/share/CHANGELOG.md | 4 ++ .../android/app/src/main/AndroidManifest.xml | 9 +--- .../shareexample/EmbeddingV1Activity.java | 21 -------- .../shareexample/EmbeddingV1ActivityTest.java | 18 ------- .../url_launcher/url_launcher/CHANGELOG.md | 4 ++ .../EmbeddingV1ActivityTest.java | 18 ------- .../android/app/src/main/AndroidManifest.xml | 16 +----- .../EmbeddingV1Activity.java | 22 --------- .../url_launcher/url_launcher/pubspec.yaml | 2 +- .../video_player/video_player/CHANGELOG.md | 4 ++ .../android/app/src/main/AndroidManifest.xml | 13 ----- .../EmbeddingV1Activity.java | 21 -------- .../video_player/video_player/pubspec.yaml | 2 +- .../webview_flutter/CHANGELOG.md | 4 ++ .../EmbeddingV1ActivityTest.java | 18 ------- .../android/app/src/main/AndroidManifest.xml | 9 +--- .../webview_flutter/pubspec.yaml | 2 +- 93 files changed, 123 insertions(+), 1084 deletions(-) delete mode 100644 packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/PluginRegistrantException.java delete mode 100644 packages/android_alarm_manager/example/android/app/src/main/java/io/flutter/plugins/androidalarmmanagerexample/Application.java delete mode 100644 packages/android_alarm_manager/example/android/app/src/main/java/io/flutter/plugins/androidalarmmanagerexample/EmbeddingV1Activity.java delete mode 100644 packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/EmbeddingV1ActivityTest.java delete mode 100644 packages/android_intent/example/android/app/src/main/java/io/flutter/plugins/androidintentexample/EmbeddingV1Activity.java delete mode 100644 packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/EmbeddingV1ActivityTest.java delete mode 100644 packages/camera/camera/example/android/app/src/main/java/io/flutter/plugins/cameraexample/EmbeddingV1Activity.java delete mode 100644 packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/EmbeddingV1Activity.java delete mode 100644 packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/EmbeddingV1ActivityTest.java delete mode 100644 packages/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1Activity.java delete mode 100644 packages/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1ActivityTest.java delete mode 100644 packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/EmbeddingV1ActivityTest.java delete mode 100644 packages/flutter_plugin_android_lifecycle/example/android/app/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle_example/EmbeddingV1Activity.java delete mode 100644 packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/EmbeddingV1ActivityTest.java delete mode 100644 packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/java/io/flutter/plugins/googlemapsexample/EmbeddingV1Activity.java delete mode 100644 packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java delete mode 100644 packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java delete mode 100644 packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java delete mode 100644 packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java delete mode 100644 packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1Activity.java delete mode 100644 packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1ActivityTest.java delete mode 100644 packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1Activity.java delete mode 100644 packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1ActivityTest.java delete mode 100644 packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/EmbeddingV1ActivityTest.java delete mode 100644 packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/EmbeddingV1Activity.java delete mode 100644 packages/path_provider/path_provider/example/android/app/src/androidTest/java/EmbeddingV1ActivityTest.java delete mode 100644 packages/path_provider/path_provider/example/android/app/src/main/java/io/flutter/plugins/pathproviderexample/EmbeddingV1Activity.java delete mode 100644 packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java delete mode 100644 packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.java delete mode 100644 packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/EmbeddingV1Activity.java delete mode 100644 packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/EmbeddingV1ActivityTest.java delete mode 100644 packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1Activity.java delete mode 100644 packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1ActivityTest.java delete mode 100644 packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/EmbeddingV1ActivityTest.java delete mode 100644 packages/url_launcher/url_launcher/example/android/app/src/main/java/io/flutter/plugins/urllauncherexample/EmbeddingV1Activity.java delete mode 100644 packages/video_player/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java delete mode 100644 packages/webview_flutter/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md index 7c40428c22ba..71f47cede66e 100644 --- a/packages/android_alarm_manager/CHANGELOG.md +++ b/packages/android_alarm_manager/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Remove support for the V1 Android embedding. + ## 2.0.2 * Update README to point to Plus Plugins version. diff --git a/packages/android_alarm_manager/README.md b/packages/android_alarm_manager/README.md index 500c5d5232f9..beefa985ef10 100644 --- a/packages/android_alarm_manager/README.md +++ b/packages/android_alarm_manager/README.md @@ -74,55 +74,6 @@ will not run in the same isolate as the main application. Unlike threads, isolat memory and communication between isolates must be done via message passing (see more documentation on isolates [here](https://api.dart.dev/stable/2.0.0/dart-isolate/dart-isolate-library.html)). - -## Using other plugins in alarm callbacks - -If alarm callbacks will need access to other Flutter plugins, including the -alarm manager plugin itself, it may be necessary to inform the background service how -to initialize plugins depending on which Flutter Android embedding the application is -using. - -### Flutter Android Embedding V1 - -For the Flutter Android Embedding V1, the background service must be provided a -callback to register plugins with the background isolate. This is done by giving -the `AlarmService` a callback to call the application's `onCreate` method. See the example's -[Application overrides](https://github.com/flutter/plugins/blob/master/packages/android_alarm_manager/example/android/app/src/main/java/io/flutter/plugins/androidalarmmanagerexample/Application.java). - -In particular, its `Application` class is as follows: - -```java -public class Application extends FlutterApplication implements PluginRegistrantCallback { - @Override - public void onCreate() { - super.onCreate(); - AlarmService.setPluginRegistrant(this); - } - - @Override - public void registerWith(PluginRegistry registry) { - GeneratedPluginRegistrant.registerWith(registry); - } -} -``` - -Which must be reflected in the application's `AndroidManifest.xml`. E.g.: - -```xml - *

  • The given {@code callbackHandle} must correspond to a registered Dart callback. If the * handle does not resolve to a Dart callback then this method does nothing. - *
  • A static {@link #pluginRegistrantCallback} must exist, otherwise a {@link - * PluginRegistrantException} will be thrown. * */ public static void startBackgroundIsolate(Context context, long callbackHandle) { @@ -89,23 +87,6 @@ public static void setCallbackDispatcher(Context context, long callbackHandle) { FlutterBackgroundExecutor.setCallbackDispatcher(context, callbackHandle); } - /** - * Sets the {@link io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback} used to - * register the plugins used by an application with the newly spawned background isolate. - * - *

    This should be invoked in {@link Application.onCreate} with {@link - * GeneratedPluginRegistrant} in applications using the V1 embedding API in order to use other - * plugins in the background isolate. For applications using the V2 embedding API, it is not - * necessary to set a {@link io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback} as - * plugins are registered automatically. - */ - @SuppressWarnings("deprecation") - public static void setPluginRegistrant( - io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback callback) { - // Indirectly set in FlutterBackgroundExecutor for backwards compatibility. - FlutterBackgroundExecutor.setPluginRegistrant(callback); - } - private static void scheduleAlarm( Context context, int requestCode, diff --git a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AndroidAlarmManagerPlugin.java b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AndroidAlarmManagerPlugin.java index fd3a9c5e87dd..45f047b5ae68 100644 --- a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AndroidAlarmManagerPlugin.java +++ b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AndroidAlarmManagerPlugin.java @@ -147,8 +147,6 @@ public void onMethodCall(MethodCall call, Result result) { } } catch (JSONException e) { result.error("error", "JSON error: " + e.getMessage(), null); - } catch (PluginRegistrantException e) { - result.error("error", "AlarmManager error: " + e.getMessage(), null); } } diff --git a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/FlutterBackgroundExecutor.java b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/FlutterBackgroundExecutor.java index d9c40bfe7181..0aa08ed216e0 100644 --- a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/FlutterBackgroundExecutor.java +++ b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/FlutterBackgroundExecutor.java @@ -45,20 +45,6 @@ public class FlutterBackgroundExecutor implements MethodCallHandler { private AtomicBoolean isCallbackDispatcherReady = new AtomicBoolean(false); - /** - * Sets the {@code io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback} used to - * register plugins with the newly spawned isolate. - * - *

    Note: this is only necessary for applications using the V1 engine embedding API as plugins - * are automatically registered via reflection in the V2 engine embedding API. If not set, alarm - * callbacks will not be able to utilize functionality from other plugins. - */ - @SuppressWarnings("deprecation") - public static void setPluginRegistrant( - io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback callback) { - pluginRegistrantCallback = callback; - } - /** * Sets the Dart callback handle for the Dart method that is responsible for initializing the * background Dart isolate, preparing it to receive Dart callback tasks requests. @@ -81,19 +67,15 @@ private void onInitialized() { @Override public void onMethodCall(MethodCall call, Result result) { String method = call.method; - try { - if (method.equals("AlarmService.initialized")) { - // This message is sent by the background method channel as soon as the background isolate - // is running. From this point forward, the Android side of this plugin can send - // callback handles through the background method channel, and the Dart side will execute - // the Dart methods corresponding to those callback handles. - onInitialized(); - result.success(true); - } else { - result.notImplemented(); - } - } catch (PluginRegistrantException e) { - result.error("error", "AlarmManager error: " + e.getMessage(), null); + if (method.equals("AlarmService.initialized")) { + // This message is sent by the background method channel as soon as the background isolate + // is running. From this point forward, the Android side of this plugin can send + // callback handles through the background method channel, and the Dart side will execute + // the Dart methods corresponding to those callback handles. + onInitialized(); + result.success(true); + } else { + result.notImplemented(); } } @@ -115,8 +97,6 @@ public void onMethodCall(MethodCall call, Result result) { *

      *
    • The given callback must correspond to a registered Dart callback. If the handle does not * resolve to a Dart callback then this method does nothing. - *
    • A static {@link #pluginRegistrantCallback} must exist, otherwise a {@link - * PluginRegistrantException} will be thrown. *
    */ public void startBackgroundIsolate(Context context) { @@ -143,8 +123,6 @@ public void startBackgroundIsolate(Context context) { *
      *
    • The given {@code callbackHandle} must correspond to a registered Dart callback. If the * handle does not resolve to a Dart callback then this method does nothing. - *
    • A static {@link #pluginRegistrantCallback} must exist, otherwise a {@link - * PluginRegistrantException} will be thrown. *
    */ public void startBackgroundIsolate(Context context, long callbackHandle) { diff --git a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/PluginRegistrantException.java b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/PluginRegistrantException.java deleted file mode 100644 index afbc1c71bd3f..000000000000 --- a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/PluginRegistrantException.java +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.androidalarmmanager; - -class PluginRegistrantException extends RuntimeException { - public PluginRegistrantException() { - super( - "PluginRegistrantCallback is not set. Did you forget to call " - + "AlarmService.setPluginRegistrant? See the README for instructions."); - } -} diff --git a/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml b/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml index 2a9dc331ebf1..2fef38483800 100644 --- a/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml +++ b/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml @@ -6,18 +6,8 @@ - - rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/android_intent/example/android/app/src/main/AndroidManifest.xml b/packages/android_intent/example/android/app/src/main/AndroidManifest.xml index 761c35fd64d8..e0aa7f84d7b9 100644 --- a/packages/android_intent/example/android/app/src/main/AndroidManifest.xml +++ b/packages/android_intent/example/android/app/src/main/AndroidManifest.xml @@ -1,23 +1,8 @@ - - - - - + android:label="android_intent_example"> rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml b/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml index f216a7251bcf..cef23162ddb6 100644 --- a/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml +++ b/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml @@ -3,20 +3,7 @@ - - - + android:label="camera_example"> =2.12.0 <3.0.0" diff --git a/packages/connectivity/connectivity/CHANGELOG.md b/packages/connectivity/connectivity/CHANGELOG.md index 89db7aeba9bb..58047482fcb7 100644 --- a/packages/connectivity/connectivity/CHANGELOG.md +++ b/packages/connectivity/connectivity/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Remove references to the Android V1 embedding. + ## 3.0.6 * Update README to point to Plus Plugins version. diff --git a/packages/connectivity/connectivity/example/android/app/src/main/AndroidManifest.xml b/packages/connectivity/connectivity/example/android/app/src/main/AndroidManifest.xml index 902642e0ca49..abce0da89989 100644 --- a/packages/connectivity/connectivity/example/android/app/src/main/AndroidManifest.xml +++ b/packages/connectivity/connectivity/example/android/app/src/main/AndroidManifest.xml @@ -3,15 +3,7 @@ - - - + rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/device_info/device_info/CHANGELOG.md b/packages/device_info/device_info/CHANGELOG.md index a92cb8ce94b1..669423cc4efb 100644 --- a/packages/device_info/device_info/CHANGELOG.md +++ b/packages/device_info/device_info/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Remove references to the Android V1 embedding. + ## 2.0.2 * Update README to point to Plus Plugins version. diff --git a/packages/device_info/device_info/example/android/app/src/main/AndroidManifest.xml b/packages/device_info/device_info/example/android/app/src/main/AndroidManifest.xml index f9f91fa39dae..4268475986a3 100644 --- a/packages/device_info/device_info/example/android/app/src/main/AndroidManifest.xml +++ b/packages/device_info/device_info/example/android/app/src/main/AndroidManifest.xml @@ -3,16 +3,7 @@ - - - - + - + diff --git a/packages/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1Activity.java b/packages/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1Activity.java deleted file mode 100644 index 86966cd137bb..000000000000 --- a/packages/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.deviceinfoexample; - -import android.os.Bundle; -import io.flutter.plugins.deviceinfo.DeviceInfoPlugin; - -@SuppressWarnings("deprecation") -public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - DeviceInfoPlugin.registerWith(registrarFor("io.flutter.plugins.deviceinfo.DeviceInfoPlugin")); - } -} diff --git a/packages/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1ActivityTest.java b/packages/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index a9babfe803ae..000000000000 --- a/packages/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.deviceinfoexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -@SuppressWarnings("deprecation") -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/espresso/CHANGELOG.md b/packages/espresso/CHANGELOG.md index 4699db18c579..10e5ae59f71a 100644 --- a/packages/espresso/CHANGELOG.md +++ b/packages/espresso/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.0+3 + +* Remove references to the Android v1 embedding. + ## 0.1.0+2 * Migrate maven repo from jcenter to mavenCentral diff --git a/packages/espresso/example/android/app/src/main/AndroidManifest.xml b/packages/espresso/example/android/app/src/main/AndroidManifest.xml index b82df920d3bc..366373e997dc 100644 --- a/packages/espresso/example/android/app/src/main/AndroidManifest.xml +++ b/packages/espresso/example/android/app/src/main/AndroidManifest.xml @@ -1,12 +1,6 @@ - =2.12.0 <3.0.0" diff --git a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md index f24a22332eaa..6a05ed01e2de 100644 --- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md +++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md @@ -1,9 +1,14 @@ +## 2.0.3 + +* Remove references to the Android V1 embedding. + ## 2.0.2 -* Migrate maven repo from jcenter to mavenCentral + +* Migrate maven repo from jcenter to mavenCentral. ## 2.0.1 -* Make sure androidx.lifecycle.DefaultLifecycleObservable doesn't get shrunk - away. + +* Make sure androidx.lifecycle.DefaultLifecycleObservable doesn't get shrunk away. ## 2.0.0 diff --git a/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/EmbeddingV1ActivityTest.java b/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/EmbeddingV1ActivityTest.java deleted file mode 100644 index 84173f4a9c0f..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.flutter_plugin_android_lifecycle_example; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -@SuppressWarnings("deprecation") -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/AndroidManifest.xml b/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/AndroidManifest.xml index 74f1397fc707..d00868f25cbf 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/AndroidManifest.xml +++ b/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/AndroidManifest.xml @@ -1,12 +1,6 @@ - - - diff --git a/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle_example/EmbeddingV1Activity.java b/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle_example/EmbeddingV1Activity.java deleted file mode 100644 index e6ab004fccf6..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle_example/EmbeddingV1Activity.java +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.flutter_plugin_android_lifecycle_example; - -import android.os.Bundle; -import dev.flutter.plugins.integration_test.IntegrationTestPlugin; -import io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin; - -@SuppressWarnings("deprecation") -public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - IntegrationTestPlugin.registerWith( - registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin")); - FlutterAndroidLifecyclePlugin.registerWith( - registrarFor( - "io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin")); - } -} diff --git a/packages/flutter_plugin_android_lifecycle/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/pubspec.yaml index 2fefc8616868..0fc128d03e17 100644 --- a/packages/flutter_plugin_android_lifecycle/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_plugin_android_lifecycle description: Flutter plugin for accessing an Android Lifecycle within other plugins. repository: https://github.com/flutter/plugins/tree/master/packages/flutter_plugin_android_lifecycle issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_plugin_android_lifecycle%22 -version: 2.0.2 +version: 2.0.3 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 04be1b915a5a..6ffec4e65cc4 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,7 +1,8 @@ -## NEXT +## 2.0.7 * Add iOS unit and UI integration test targets. * Exclude arm64 simulators in example app. +* Remove references to the Android V1 embedding. ## 2.0.6 diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/EmbeddingV1ActivityTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/EmbeddingV1ActivityTest.java deleted file mode 100644 index 9da7185b8ace..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.googlemaps; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import io.flutter.plugins.googlemapsexample.*; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -@SuppressWarnings("deprecation") -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/AndroidManifest.xml index 0ff45c3cb3ac..815074bfad96 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/AndroidManifest.xml +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/AndroidManifest.xml @@ -4,10 +4,7 @@ - + @@ -28,13 +25,6 @@ - - diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/java/io/flutter/plugins/googlemapsexample/EmbeddingV1Activity.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/java/io/flutter/plugins/googlemapsexample/EmbeddingV1Activity.java deleted file mode 100644 index cecf76a690e0..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/java/io/flutter/plugins/googlemapsexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.googlemapsexample; - -import android.os.Bundle; -import dev.flutter.plugins.integration_test.IntegrationTestPlugin; -import io.flutter.plugins.googlemaps.GoogleMapsPlugin; - -@SuppressWarnings("deprecation") -public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GoogleMapsPlugin.registerWith(registrarFor("io.flutter.plugins.googlemaps.GoogleMapsPlugin")); - IntegrationTestPlugin.registerWith( - registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin")); - } -} diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 0d7475857b31..c784e9a37a94 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.0.6 +version: 2.0.7 environment: sdk: '>=2.12.0 <3.0.0' diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 186a1d39a223..2602e98be2a0 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.0.6 + +* Remove references to the Android V1 embedding. + ## 5.0.5 * Add iOS unit and UI integration test targets. diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/main/AndroidManifest.xml b/packages/google_sign_in/google_sign_in/example/android/app/src/main/AndroidManifest.xml index df80f829c1e7..22a34d7218f7 100644 --- a/packages/google_sign_in/google_sign_in/example/android/app/src/main/AndroidManifest.xml +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/main/AndroidManifest.xml @@ -14,12 +14,6 @@ - - diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java b/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java deleted file mode 100644 index f61bb72ba9da..000000000000 --- a/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.googlesigninexample; - -import android.os.Bundle; -import io.flutter.plugins.googlesignin.GoogleSignInPlugin; -import io.flutter.view.FlutterMain; - -@SuppressWarnings("deprecation") -public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - FlutterMain.startInitialization(this); - super.onCreate(savedInstanceState); - GoogleSignInPlugin.registerWith(registrarFor("io.flutter.plugins.googlesignin")); - } -} diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java b/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index cfd2fcec9ec3..000000000000 --- a/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.googlesigninexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -@SuppressWarnings("deprecation") -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index 14f7d8901301..bbcdbc91d71e 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. repository: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 5.0.5 +version: 5.0.6 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/image_picker/image_picker/example/android/app/src/main/AndroidManifest.xml b/packages/image_picker/image_picker/example/android/app/src/main/AndroidManifest.xml index 597abd9b81ab..543fca922e1b 100755 --- a/packages/image_picker/image_picker/example/android/app/src/main/AndroidManifest.xml +++ b/packages/image_picker/image_picker/example/android/app/src/main/AndroidManifest.xml @@ -14,13 +14,6 @@ - - diff --git a/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java b/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java deleted file mode 100644 index b9d2808a4486..000000000000 --- a/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.imagepickerexample; - -import android.os.Bundle; -import io.flutter.plugins.imagepicker.ImagePickerPlugin; -import io.flutter.plugins.videoplayer.VideoPlayerPlugin; - -@SuppressWarnings("deprecation") -public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ImagePickerPlugin.registerWith( - registrarFor("io.flutter.plugins.imagepicker.ImagePickerPlugin")); - VideoPlayerPlugin.registerWith( - registrarFor("io.flutter.plugins.videoplayer.VideoPlayerPlugin")); - } -} diff --git a/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java b/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index 7d790563abae..000000000000 --- a/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.imagepickerexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -@SuppressWarnings("deprecation") -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md index 52bbff52bef0..228fcddb6370 100644 --- a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.7 + +* Remove references to the Android V1 embedding. + ## 1.0.6 * Added import flutter foundation dependency in README.md to be able to use `defaultTargetPlatform`. diff --git a/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/AndroidManifest.xml b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/AndroidManifest.xml index a17382b97d83..027375c09e04 100644 --- a/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/AndroidManifest.xml +++ b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/AndroidManifest.xml @@ -6,32 +6,7 @@ to allow setting breakpoints, to provide hot reload, etc. --> - - - - - - - + rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/in_app_purchase/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/pubspec.yaml index 554a07b0bd30..a37ae07baa86 100644 --- a/packages/in_app_purchase/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 1.0.6 +version: 1.0.7 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/AndroidManifest.xml b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/AndroidManifest.xml index a17382b97d83..1185a05b3530 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/AndroidManifest.xml +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/AndroidManifest.xml @@ -6,32 +6,9 @@ to allow setting breakpoints, to provide hot reload, etc. --> - - - - - - rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/local_auth/CHANGELOG.md b/packages/local_auth/CHANGELOG.md index a97c4b47b288..c33fa7778b94 100644 --- a/packages/local_auth/CHANGELOG.md +++ b/packages/local_auth/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.7 + +* Remove references to the Android V1 embedding. + ## 1.1.6 * Migrate maven repository from jcenter to mavenCentral. diff --git a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/EmbeddingV1ActivityTest.java b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/EmbeddingV1ActivityTest.java deleted file mode 100644 index 696fc493c6b8..000000000000 --- a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.localauth; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import io.flutter.plugins.localauthexample.EmbeddingV1Activity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -@SuppressWarnings("deprecation") -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/local_auth/example/android/app/src/main/AndroidManifest.xml b/packages/local_auth/example/android/app/src/main/AndroidManifest.xml index 1425d9c6ab62..8c091772107a 100644 --- a/packages/local_auth/example/android/app/src/main/AndroidManifest.xml +++ b/packages/local_auth/example/android/app/src/main/AndroidManifest.xml @@ -4,7 +4,7 @@ - + - - diff --git a/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/EmbeddingV1Activity.java b/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/EmbeddingV1Activity.java deleted file mode 100644 index c3fc8d47b3a4..000000000000 --- a/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.localauthexample; - -import android.os.Bundle; -import dev.flutter.plugins.integration_test.IntegrationTestPlugin; -import io.flutter.app.FlutterFragmentActivity; -import io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin; -import io.flutter.plugins.localauth.LocalAuthPlugin; - -@SuppressWarnings("deprecation") -public class EmbeddingV1Activity extends FlutterFragmentActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - IntegrationTestPlugin.registerWith( - registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin")); - FlutterAndroidLifecyclePlugin.registerWith( - registrarFor( - "io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin")); - LocalAuthPlugin.registerWith(registrarFor("io.flutter.plugins.localauth.LocalAuthPlugin")); - } -} diff --git a/packages/local_auth/pubspec.yaml b/packages/local_auth/pubspec.yaml index f50492381586..8a31b2f7d501 100644 --- a/packages/local_auth/pubspec.yaml +++ b/packages/local_auth/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Android and iOS devices to allow local authentication via fingerprint, touch ID, face ID, passcode, pin, or pattern. repository: https://github.com/flutter/plugins/tree/master/packages/local_auth issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.1.6 +version: 1.1.7 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/package_info/CHANGELOG.md b/packages/package_info/CHANGELOG.md index 96697dd220e6..2ec20b3fe775 100644 --- a/packages/package_info/CHANGELOG.md +++ b/packages/package_info/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Remove references to the Android v1 embedding. + ## 2.0.2 * Update README to point to Plus Plugins version. diff --git a/packages/package_info/example/android/app/src/main/AndroidManifest.xml b/packages/package_info/example/android/app/src/main/AndroidManifest.xml index e4d033e8d8dd..f5544ce31f9f 100644 --- a/packages/package_info/example/android/app/src/main/AndroidManifest.xml +++ b/packages/package_info/example/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ - + rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/path_provider/path_provider/example/android/app/src/main/AndroidManifest.xml b/packages/path_provider/path_provider/example/android/app/src/main/AndroidManifest.xml index ec8e31f5172b..df8cee7bc3be 100644 --- a/packages/path_provider/path_provider/example/android/app/src/main/AndroidManifest.xml +++ b/packages/path_provider/path_provider/example/android/app/src/main/AndroidManifest.xml @@ -3,13 +3,7 @@ - - - + =2.12.0 <3.0.0" diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md index b917dcc85db0..4f8943845cf7 100644 --- a/packages/quick_actions/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.0+4 + +* Remove references to the Android V1 embedding. + ## 0.6.0+3 * Added a `const` constructor for the `QuickActions` class, so the plugin will behave as documented in the sample code mentioned in the [README.md](https://github.com/flutter/plugins/blob/59e16a556e273c2d69189b2dcdfa92d101ea6408/packages/quick_actions/quick_actions/README.md). diff --git a/packages/quick_actions/quick_actions/example/android/app/src/main/AndroidManifest.xml b/packages/quick_actions/quick_actions/example/android/app/src/main/AndroidManifest.xml index 56c924e5c8b5..4f384b7c6b13 100644 --- a/packages/quick_actions/quick_actions/example/android/app/src/main/AndroidManifest.xml +++ b/packages/quick_actions/quick_actions/example/android/app/src/main/AndroidManifest.xml @@ -3,30 +3,20 @@ - - + - - - - - - - + + + + + + diff --git a/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java b/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java deleted file mode 100644 index d85ead3b4e36..000000000000 --- a/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.quickactionsexample; - -import android.os.Bundle; -import io.flutter.plugins.quickactions.QuickActionsPlugin; - -@SuppressWarnings("deprecation") -public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - QuickActionsPlugin.registerWith( - registrarFor("io.flutter.plugins.quickactions.QuickActionsPlugin")); - } -} diff --git a/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.java b/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index a7fab3f052a4..000000000000 --- a/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.quickactionsexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -@SuppressWarnings("deprecation") -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/quick_actions/quick_actions/pubspec.yaml b/packages/quick_actions/quick_actions/pubspec.yaml index 2a4fb0c634e0..657c2f001a83 100644 --- a/packages/quick_actions/quick_actions/pubspec.yaml +++ b/packages/quick_actions/quick_actions/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for creating shortcuts on home screen, also known as Quick Actions on iOS and App Shortcuts on Android. repository: https://github.com/flutter/plugins/tree/master/packages/quick_actions issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+quick_actions%22 -version: 0.6.0+3 +version: 0.6.0+4 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/sensors/CHANGELOG.md b/packages/sensors/CHANGELOG.md index d7bf66d432a6..5ac0943333fa 100644 --- a/packages/sensors/CHANGELOG.md +++ b/packages/sensors/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Remove references to the Android V1 embedding. + ## 2.0.3 * Update README to point to Plus Plugins version. diff --git a/packages/sensors/example/android/app/src/main/AndroidManifest.xml b/packages/sensors/example/android/app/src/main/AndroidManifest.xml index 5c12a301b623..ea3155cb9722 100644 --- a/packages/sensors/example/android/app/src/main/AndroidManifest.xml +++ b/packages/sensors/example/android/app/src/main/AndroidManifest.xml @@ -3,14 +3,7 @@ - - - + rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/share/CHANGELOG.md b/packages/share/CHANGELOG.md index a5e45110ebeb..9074f59f05b7 100644 --- a/packages/share/CHANGELOG.md +++ b/packages/share/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Remove references to the Android V1 embedding. + ## 2.0.4 * Update README to point to Plus Plugins version. diff --git a/packages/share/example/android/app/src/main/AndroidManifest.xml b/packages/share/example/android/app/src/main/AndroidManifest.xml index 350fdaf5839a..d1f1ce953e3a 100644 --- a/packages/share/example/android/app/src/main/AndroidManifest.xml +++ b/packages/share/example/android/app/src/main/AndroidManifest.xml @@ -3,14 +3,7 @@ - - - + rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 1dcf7a1582a8..dc67a2142ec2 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.0.10 + +* Remove references to the Android v1 embedding. + ## 6.0.9 * Silenced warnings that may occur during build when using a very diff --git a/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/EmbeddingV1ActivityTest.java b/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index 4fb52708b9eb..000000000000 --- a/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.urllauncherexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -@SuppressWarnings("deprecation") -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml b/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml index d6753c9bbdbc..918c29ee2dca 100644 --- a/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml +++ b/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml @@ -19,23 +19,9 @@ - - - + android:label="url_launcher_example"> =2.12.0 <3.0.0" diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index b9f029b31454..ec61f87f5086 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.11 + +* Remove references to the Android V1 embedding. + ## 2.1.10 * Ensure video pauses correctly when it finishes. diff --git a/packages/video_player/video_player/example/android/app/src/main/AndroidManifest.xml b/packages/video_player/video_player/example/android/app/src/main/AndroidManifest.xml index 3ad2e146c2e1..a2574c90d7d9 100644 --- a/packages/video_player/video_player/example/android/app/src/main/AndroidManifest.xml +++ b/packages/video_player/video_player/example/android/app/src/main/AndroidManifest.xml @@ -4,20 +4,7 @@ - - - =2.12.0 <3.0.0" diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 46f5e045ddd8..4ffdb08928c2 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.11 + +* Remove references to the Android V1 embedding. + ## 2.0.10 * Fix keyboard issues link in the README. diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index 56691d2fc82a..000000000000 --- a/packages/webview_flutter/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutterexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -@SuppressWarnings("deprecation") -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml index 02f270fb9c49..945e47c29e82 100644 --- a/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml +++ b/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml @@ -1,15 +1,8 @@ - - + android:label="webview_flutter_example"> diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 88ab4ad7927e..2f00071e772e 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.0.10 +version: 2.0.11 environment: sdk: ">=2.12.0 <3.0.0" From 0f266188d9beeb0abf75c820f991a7a192c5a203 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Fri, 23 Jul 2021 14:48:55 -0700 Subject: [PATCH 024/123] Remove remaining V1 embedding references (#4189) --- packages/battery/battery/CHANGELOG.md | 4 ++++ .../battery/EmbedderV1ActivityTest.java | 17 --------------- .../android/app/src/main/AndroidManifest.xml | 7 ------- .../batteryexample/EmbedderV1Activity.java | 20 ------------------ .../EmbedderV1ActivityTest.java | 17 --------------- .../android/app/src/main/AndroidManifest.xml | 6 ------ .../EmbedderV1Activity.java | 21 ------------------- 7 files changed, 4 insertions(+), 88 deletions(-) delete mode 100644 packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/EmbedderV1ActivityTest.java delete mode 100644 packages/battery/battery/example/android/app/src/main/java/io/flutter/plugins/batteryexample/EmbedderV1Activity.java delete mode 100644 packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/EmbedderV1ActivityTest.java delete mode 100644 packages/package_info/example/android/app/src/main/java/io/flutter/plugins/packageinfoexample/EmbedderV1Activity.java diff --git a/packages/battery/battery/CHANGELOG.md b/packages/battery/battery/CHANGELOG.md index b4f0cef94edb..8590e646564e 100644 --- a/packages/battery/battery/CHANGELOG.md +++ b/packages/battery/battery/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Remove references to the Android v1 embedding. + ## 2.0.3 * Update README to point to Plus Plugins version. diff --git a/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/EmbedderV1ActivityTest.java b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/EmbedderV1ActivityTest.java deleted file mode 100644 index c939be4281da..000000000000 --- a/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/EmbedderV1ActivityTest.java +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.batteryexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -public class EmbedderV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbedderV1Activity.class); -} diff --git a/packages/battery/battery/example/android/app/src/main/AndroidManifest.xml b/packages/battery/battery/example/android/app/src/main/AndroidManifest.xml index d44a8ac5757a..11feb41de96a 100644 --- a/packages/battery/battery/example/android/app/src/main/AndroidManifest.xml +++ b/packages/battery/battery/example/android/app/src/main/AndroidManifest.xml @@ -15,13 +15,6 @@ - - diff --git a/packages/battery/battery/example/android/app/src/main/java/io/flutter/plugins/batteryexample/EmbedderV1Activity.java b/packages/battery/battery/example/android/app/src/main/java/io/flutter/plugins/batteryexample/EmbedderV1Activity.java deleted file mode 100644 index 2b9e538bbe47..000000000000 --- a/packages/battery/battery/example/android/app/src/main/java/io/flutter/plugins/batteryexample/EmbedderV1Activity.java +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.batteryexample; - -import android.os.Bundle; -import dev.flutter.plugins.integration_test.IntegrationTestPlugin; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.battery.BatteryPlugin; - -public class EmbedderV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - BatteryPlugin.registerWith(registrarFor("io.flutter.plugins.battery.BatteryPlugin")); - IntegrationTestPlugin.registerWith( - registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin")); - } -} diff --git a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/EmbedderV1ActivityTest.java b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/EmbedderV1ActivityTest.java deleted file mode 100644 index 8d3b0b6c6cad..000000000000 --- a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/EmbedderV1ActivityTest.java +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.packageinfoexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -public class EmbedderV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbedderV1Activity.class); -} diff --git a/packages/package_info/example/android/app/src/main/AndroidManifest.xml b/packages/package_info/example/android/app/src/main/AndroidManifest.xml index f5544ce31f9f..efb42ac02c5c 100644 --- a/packages/package_info/example/android/app/src/main/AndroidManifest.xml +++ b/packages/package_info/example/android/app/src/main/AndroidManifest.xml @@ -14,12 +14,6 @@ - - diff --git a/packages/package_info/example/android/app/src/main/java/io/flutter/plugins/packageinfoexample/EmbedderV1Activity.java b/packages/package_info/example/android/app/src/main/java/io/flutter/plugins/packageinfoexample/EmbedderV1Activity.java deleted file mode 100644 index ded5f348c506..000000000000 --- a/packages/package_info/example/android/app/src/main/java/io/flutter/plugins/packageinfoexample/EmbedderV1Activity.java +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.packageinfoexample; - -import android.os.Bundle; -import dev.flutter.plugins.integration_test.IntegrationTestPlugin; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.packageinfo.PackageInfoPlugin; - -public class EmbedderV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - PackageInfoPlugin.registerWith( - registrarFor("io.flutter.plugins.packageinfo.PackageInfoPlugin")); - IntegrationTestPlugin.registerWith( - registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin")); - } -} From 400fa202592a4c6021789a91b029de904c4c6ada Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Sat, 24 Jul 2021 02:31:06 +0200 Subject: [PATCH 025/123] [camera_web] Add `initializeCamera` implementation (#4186) --- .../example/integration_test/camera_test.dart | 52 ++++++- .../integration_test/camera_web_test.dart | 143 ++++++++++++++---- .../integration_test/helpers/mocks.dart | 23 +++ .../camera/camera_web/lib/src/camera.dart | 25 +++ .../camera/camera_web/lib/src/camera_web.dart | 56 ++++++- 5 files changed, 260 insertions(+), 39 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 0f1dcf7049d9..6eeed23ecf56 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:html'; +import 'dart:ui'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; @@ -28,13 +29,7 @@ void main() { navigator = MockNavigator(); mediaDevices = MockMediaDevices(); - final videoElement = VideoElement() - ..src = - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4' - ..preload = 'true' - ..width = 10 - ..height = 10; - + final videoElement = getVideoElementWithBlankStream(Size(10, 10)); mediaStream = videoElement.captureStream(); when(() => window.navigator).thenReturn(navigator); @@ -469,6 +464,49 @@ void main() { }); }); + group('getVideoSize', () { + testWidgets( + 'returns a size ' + 'based on the first video track settings', (tester) async { + const videoSize = Size(1280, 720); + + final videoElement = getVideoElementWithBlankStream(videoSize); + mediaStream = videoElement.captureStream(); + + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + + expect( + await camera.getVideoSize(), + equals(videoSize), + ); + }); + + testWidgets( + 'returns Size.zero ' + 'if the camera is missing video tracks', (tester) async { + // Create a video stream with no video tracks. + final videoElement = VideoElement(); + mediaStream = videoElement.captureStream(); + + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + + expect( + await camera.getVideoSize(), + equals(Size.zero), + ); + }); + }); + group('dispose', () { testWidgets('resets the video element\'s source', (tester) async { final camera = Camera( diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index eef17ecfdff9..d5e1835391ad 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -5,6 +5,7 @@ import 'dart:html'; import 'dart:ui'; +import 'package:async/async.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/camera_web.dart'; import 'package:camera_web/src/camera.dart'; @@ -33,13 +34,8 @@ void main() { window = MockWindow(); navigator = MockNavigator(); mediaDevices = MockMediaDevices(); - videoElement = VideoElement() - ..src = - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4' - ..preload = 'true' - ..width = 10 - ..height = 10 - ..crossOrigin = 'anonymous'; + + videoElement = getVideoElementWithBlankStream(Size(10, 10)); cameraSettings = MockCameraSettings(); @@ -327,21 +323,18 @@ void main() { const ultraHighResolutionSize = Size(3840, 2160); const maxResolutionSize = Size(3840, 2160); - late CameraDescription cameraDescription; - late CameraMetadata cameraMetadata; - - setUp(() { - cameraDescription = CameraDescription( - name: 'name', - lensDirection: CameraLensDirection.front, - sensorOrientation: 0, - ); + final cameraDescription = CameraDescription( + name: 'name', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ); - cameraMetadata = CameraMetadata( - deviceId: 'deviceId', - facingMode: 'user', - ); + final cameraMetadata = CameraMetadata( + deviceId: 'deviceId', + facingMode: 'user', + ); + setUp(() { // Add metadata for the camera description. (CameraPlatform.instance as CameraPlugin) .camerasMetadata[cameraDescription] = cameraMetadata; @@ -434,11 +427,38 @@ void main() { }); }); - testWidgets('initializeCamera throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.initializeCamera(cameraId), - throwsUnimplementedError, - ); + group('initializeCamera', () { + testWidgets( + 'throws CameraException ' + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notFound, + ), + ), + ); + }); + + testWidgets('initializes and plays the camera', (tester) async { + final camera = MockCamera(); + + when(camera.getVideoSize).thenAnswer((_) => Future.value(Size(10, 10))); + when(camera.initialize).thenAnswer((_) => Future.value()); + when(camera.play).thenAnswer((_) => Future.value()); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + + verify(camera.initialize).called(1); + verify(camera.play).called(1); + }); }); testWidgets('lockCaptureOrientation throws UnimplementedError', @@ -628,13 +648,78 @@ void main() { ); }); + group('getCamera', () { + testWidgets('returns the correct camera', (tester) async { + final camera = Camera(textureId: cameraId, window: window); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + (CameraPlatform.instance as CameraPlugin).getCamera(cameraId), + equals(camera), + ); + }); + + testWidgets( + 'throws CameraException ' + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => (CameraPlatform.instance as CameraPlugin).getCamera(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notFound, + ), + ), + ); + }); + }); + group('events', () { - testWidgets('onCameraInitialized throws UnimplementedError', - (tester) async { + testWidgets( + 'onCameraInitialized emits a CameraInitializedEvent ' + 'on initializeCamera', (tester) async { + // Mock the camera to use a blank video stream of size 1280x720. + const videoSize = Size(1280, 720); + + videoElement = getVideoElementWithBlankStream(videoSize); + + when( + () => mediaDevices.getUserMedia(any()), + ).thenAnswer((_) async => videoElement.captureStream()); + + final camera = Camera( + textureId: cameraId, + window: window, + ); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final Stream eventStream = + CameraPlatform.instance.onCameraInitialized(cameraId); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + expect( - () => CameraPlatform.instance.onCameraInitialized(cameraId), - throwsUnimplementedError, + await streamQueue.next, + CameraInitializedEvent( + cameraId, + videoSize.width, + videoSize.height, + ExposureMode.auto, + false, + FocusMode.auto, + false, + ), ); + + await streamQueue.cancel(); }); testWidgets('onCameraResolutionChanged throws UnimplementedError', diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart index 3702aee8e184..fa627ca0b7e6 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -3,7 +3,9 @@ // found in the LICENSE file. import 'dart:html'; +import 'dart:ui'; +import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_settings.dart'; import 'package:mocktail/mocktail.dart'; @@ -17,6 +19,8 @@ class MockCameraSettings extends Mock implements CameraSettings {} class MockMediaStreamTrack extends Mock implements MediaStreamTrack {} +class MockCamera extends Mock implements Camera {} + /// A fake [MediaStream] that returns the provided [_videoTracks]. class FakeMediaStream extends Fake implements MediaStream { FakeMediaStream(this._videoTracks); @@ -54,3 +58,22 @@ class FakeDomException extends Fake implements DomException { @override String get name => _name; } + +/// Returns a video element with a blank stream of size [videoSize]. +/// +/// Can be used to mock a video stream: +/// ```dart +/// final videoElement = getVideoElementWithBlankStream(Size(100, 100)); +/// final videoStream = videoElement.captureStream(); +/// ``` +VideoElement getVideoElementWithBlankStream(Size videoSize) { + final canvasElement = CanvasElement( + width: videoSize.width.toInt(), + height: videoSize.height.toInt(), + )..context2D.fillRect(0, 0, videoSize.width, videoSize.height); + + final videoElement = VideoElement() + ..srcObject = canvasElement.captureStream(); + + return videoElement; +} diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 41692d548882..334f117be274 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:html' as html; +import 'dart:ui'; import 'shims/dart_ui.dart' as ui; import 'package:camera_platform_interface/camera_platform_interface.dart'; @@ -171,6 +172,30 @@ class Camera { return XFile(html.Url.createObjectUrl(blob)); } + /// Returns a size of the camera video based on its first video track size. + /// + /// Returns [Size.zero] if the camera is missing a video track or + /// the video track does not include the width or height setting. + Future getVideoSize() async { + final videoTracks = videoElement.srcObject?.getVideoTracks() ?? []; + + if (videoTracks.isEmpty) { + return Size.zero; + } + + final defaultVideoTrack = videoTracks.first; + final defaultVideoTrackSettings = defaultVideoTrack.getSettings(); + + final width = defaultVideoTrackSettings['width']; + final height = defaultVideoTrackSettings['height']; + + if (width != null && height != null) { + return Size(width, height); + } else { + return Size.zero; + } + } + /// Disposes the camera by stopping the camera stream /// and reloading the camera source. void dispose() { diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 80ab13d37d13..e58572e50ee4 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -13,6 +13,7 @@ import 'package:camera_web/src/types/types.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:stream_transform/stream_transform.dart'; /// The web implementation of [CameraPlatform]. /// @@ -42,6 +43,18 @@ class CameraPlugin extends CameraPlatform { @visibleForTesting final camerasMetadata = {}; + /// The controller used to broadcast different camera events. + /// + /// It is `broadcast` as multiple controllers may subscribe + /// to different stream views of this controller. + @visibleForTesting + final cameraEventStreamController = StreamController.broadcast(); + + /// Returns a stream of camera events for the given [cameraId]. + Stream _cameraEvents(int cameraId) => + cameraEventStreamController.stream + .where((event) => event.cameraId == cameraId); + /// The current browser window used to access media devices. @visibleForTesting html.Window? window = html.window; @@ -186,14 +199,34 @@ class CameraPlugin extends CameraPlatform { @override Future initializeCamera( int cameraId, { + // The image format group is currently not supported. ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, - }) { - throw UnimplementedError('initializeCamera() is not implemented.'); + }) async { + final camera = getCamera(cameraId); + + await camera.initialize(); + await camera.play(); + + final cameraSize = await camera.getVideoSize(); + + cameraEventStreamController.add( + CameraInitializedEvent( + cameraId, + cameraSize.width, + cameraSize.height, + // TODO(camera_web): Add support for exposure mode and point (https://github.com/flutter/flutter/issues/86857). + ExposureMode.auto, + false, + // TODO(camera_web): Add support for focus mode and point (https://github.com/flutter/flutter/issues/86858). + FocusMode.auto, + false, + ), + ); } @override Stream onCameraInitialized(int cameraId) { - throw UnimplementedError('onCameraInitialized() is not implemented.'); + return _cameraEvents(cameraId).whereType(); } @override @@ -348,4 +381,21 @@ class CameraPlugin extends CameraPlatform { return mediaDevices.getUserMedia(cameraOptions.toJson()); } + + /// Returns a camera for the given [cameraId]. + /// + /// Throws a [CameraException] if the camera does not exist. + @visibleForTesting + Camera getCamera(int cameraId) { + final camera = cameras[cameraId]; + + if (camera == null) { + throw CameraException( + CameraErrorCodes.notFound, + 'No camera found for the given camera id $cameraId.', + ); + } + + return camera; + } } From 43ca2627524bff1f528e63731055fae9de1d85c9 Mon Sep 17 00:00:00 2001 From: Mahesh Jamdade <31410839+maheshmnj@users.noreply.github.com> Date: Sat, 24 Jul 2021 11:03:05 +0530 Subject: [PATCH 026/123] [video player]: update the url in the readme example (#4081) --- packages/video_player/video_player/CHANGELOG.md | 4 ++++ packages/video_player/video_player/README.md | 4 ++-- packages/video_player/video_player/pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index ec61f87f5086..bfed1615f8a6 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.12 + +* Update the video url in the readme code sample + ## 2.1.11 * Remove references to the Android V1 embedding. diff --git a/packages/video_player/video_player/README.md b/packages/video_player/video_player/README.md index 7140527afb9f..a1d3d935e71c 100644 --- a/packages/video_player/video_player/README.md +++ b/packages/video_player/video_player/README.md @@ -75,7 +75,7 @@ class _VideoAppState extends State { void initState() { super.initState(); _controller = VideoPlayerController.network( - 'https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4') + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4') ..initialize().then((_) { // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. setState(() {}); @@ -129,7 +129,7 @@ This is not complete as of now. You can contribute to this section by [opening a ### Playback speed You can set the playback speed on your `_controller` (instance of `VideoPlayerController`) by -calling `_controller.setPlaybackSpeed`. `setPlaybackSpeed` takes a `double` speed value indicating +calling `_controller.setPlaybackSpeed`. `setPlaybackSpeed` takes a `double` speed value indicating the rate of playback for your video. For example, when given a value of `2.0`, your video will play at 2x the regular playback speed and so on. diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index cd444942ffe6..960f0c6ce63a 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.1.11 +version: 2.1.12 environment: sdk: ">=2.12.0 <3.0.0" From 7d49fd469a23e62dcc550f156d82243dee6533ff Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Sat, 24 Jul 2021 16:06:34 -0700 Subject: [PATCH 027/123] Make java-test output more useful (#4184) --- packages/android_alarm_manager/android/build.gradle | 13 +++++++++++++ packages/android_intent/android/build.gradle | 10 ++++++++++ packages/battery/battery/android/build.gradle | 13 +++++++++++++ packages/camera/camera/android/build.gradle | 9 +++++++++ .../connectivity/connectivity/android/build.gradle | 13 +++++++++++++ .../device_info/device_info/android/build.gradle | 13 +++++++++++++ packages/espresso/android/build.gradle | 13 +++++++++++++ .../android/build.gradle | 13 +++++++++++++ .../google_maps_flutter/android/build.gradle | 13 +++++++++++++ .../google_sign_in/android/build.gradle | 13 +++++++++++++ .../image_picker/image_picker/android/build.gradle | 13 +++++++++++++ .../in_app_purchase_android/android/build.gradle | 13 +++++++++++++ packages/local_auth/android/build.gradle | 13 +++++++++++++ packages/package_info/android/build.gradle | 13 +++++++++++++ .../path_provider/android/build.gradle | 13 +++++++++++++ .../quick_actions/android/build.gradle | 13 +++++++++++++ packages/sensors/android/build.gradle | 13 +++++++++++++ packages/share/android/build.gradle | 13 +++++++++++++ .../shared_preferences/android/build.gradle | 13 +++++++++++++ .../url_launcher/url_launcher/android/build.gradle | 10 ++++++++++ .../video_player/video_player/android/build.gradle | 13 +++++++++++++ .../webview_flutter/android/build.gradle | 13 +++++++++++++ .../wifi_info_flutter/android/build.gradle | 13 +++++++++++++ script/tool/lib/src/native_test_command.dart | 2 +- script/tool/test/native_test_command_test.dart | 6 +++--- 25 files changed, 293 insertions(+), 4 deletions(-) diff --git a/packages/android_alarm_manager/android/build.gradle b/packages/android_alarm_manager/android/build.gradle index 52a07082dded..be741097f362 100644 --- a/packages/android_alarm_manager/android/build.gradle +++ b/packages/android_alarm_manager/android/build.gradle @@ -39,6 +39,19 @@ android { lintOptions { disable 'InvalidPackage' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } dependencies { diff --git a/packages/android_intent/android/build.gradle b/packages/android_intent/android/build.gradle index adf53f94393c..b0238b7db4f3 100644 --- a/packages/android_intent/android/build.gradle +++ b/packages/android_intent/android/build.gradle @@ -36,8 +36,18 @@ android { lintOptions { disable 'InvalidPackage' } + + testOptions { unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } diff --git a/packages/battery/battery/android/build.gradle b/packages/battery/battery/android/build.gradle index 28d561f05652..1e484897c2ad 100644 --- a/packages/battery/battery/android/build.gradle +++ b/packages/battery/battery/android/build.gradle @@ -31,4 +31,17 @@ android { lintOptions { disable 'InvalidPackage' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index 65c6d26edb49..6ceed97c9a17 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera/android/build.gradle @@ -40,9 +40,18 @@ android { sourceCompatibility = '1.8' targetCompatibility = '1.8' } + + testOptions { unitTests.includeAndroidResources = true unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } diff --git a/packages/connectivity/connectivity/android/build.gradle b/packages/connectivity/connectivity/android/build.gradle index afd7d9f0a977..53a390bd74f0 100644 --- a/packages/connectivity/connectivity/android/build.gradle +++ b/packages/connectivity/connectivity/android/build.gradle @@ -36,4 +36,17 @@ android { lintOptions { disable 'InvalidPackage' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } diff --git a/packages/device_info/device_info/android/build.gradle b/packages/device_info/device_info/android/build.gradle index 9b1f6470a37d..51ec2a7fb567 100644 --- a/packages/device_info/device_info/android/build.gradle +++ b/packages/device_info/device_info/android/build.gradle @@ -31,4 +31,17 @@ android { lintOptions { disable 'InvalidPackage' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } diff --git a/packages/espresso/android/build.gradle b/packages/espresso/android/build.gradle index 74988a50a3b9..8cd54811afa0 100644 --- a/packages/espresso/android/build.gradle +++ b/packages/espresso/android/build.gradle @@ -31,6 +31,19 @@ android { lintOptions { disable 'InvalidPackage' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } dependencies { diff --git a/packages/flutter_plugin_android_lifecycle/android/build.gradle b/packages/flutter_plugin_android_lifecycle/android/build.gradle index cf34c98aaf3b..ba3a54b235e6 100644 --- a/packages/flutter_plugin_android_lifecycle/android/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/android/build.gradle @@ -36,6 +36,19 @@ android { dependencies { implementation "androidx.annotation:annotation:1.1.0" } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } dependencies { diff --git a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle index 1fabe10216c3..1433d3559b77 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle @@ -44,6 +44,19 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } dependencies { diff --git a/packages/google_sign_in/google_sign_in/android/build.gradle b/packages/google_sign_in/google_sign_in/android/build.gradle index c95ba17c10d7..7d1825defa84 100644 --- a/packages/google_sign_in/google_sign_in/android/build.gradle +++ b/packages/google_sign_in/google_sign_in/android/build.gradle @@ -31,6 +31,19 @@ android { lintOptions { disable 'InvalidPackage' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } dependencies { diff --git a/packages/image_picker/image_picker/android/build.gradle b/packages/image_picker/image_picker/android/build.gradle index e21b7f1738b4..e0d51d8dd1f5 100755 --- a/packages/image_picker/image_picker/android/build.gradle +++ b/packages/image_picker/image_picker/android/build.gradle @@ -36,4 +36,17 @@ android { implementation 'androidx.annotation:annotation:1.0.0' implementation 'androidx.exifinterface:exifinterface:1.3.0' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle index eeac168068f7..349f9eeb734c 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle +++ b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle @@ -35,6 +35,19 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } dependencies { diff --git a/packages/local_auth/android/build.gradle b/packages/local_auth/android/build.gradle index 4b0995e65946..4fcb77cf6c98 100644 --- a/packages/local_auth/android/build.gradle +++ b/packages/local_auth/android/build.gradle @@ -31,6 +31,19 @@ android { lintOptions { disable 'InvalidPackage' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } dependencies { diff --git a/packages/package_info/android/build.gradle b/packages/package_info/android/build.gradle index 9144e6aade58..d2846f260556 100644 --- a/packages/package_info/android/build.gradle +++ b/packages/package_info/android/build.gradle @@ -31,4 +31,17 @@ android { lintOptions { disable 'InvalidPackage' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } diff --git a/packages/path_provider/path_provider/android/build.gradle b/packages/path_provider/path_provider/android/build.gradle index 6df60f0a3a63..db2c79c15796 100644 --- a/packages/path_provider/path_provider/android/build.gradle +++ b/packages/path_provider/path_provider/android/build.gradle @@ -37,6 +37,19 @@ android { targetCompatibility 1.8 } } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } dependencies { diff --git a/packages/quick_actions/quick_actions/android/build.gradle b/packages/quick_actions/quick_actions/android/build.gradle index 00de9453f86d..038f9e99048a 100644 --- a/packages/quick_actions/quick_actions/android/build.gradle +++ b/packages/quick_actions/quick_actions/android/build.gradle @@ -31,4 +31,17 @@ android { lintOptions { disable 'InvalidPackage' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } diff --git a/packages/sensors/android/build.gradle b/packages/sensors/android/build.gradle index 50b4eac981e2..a16ebd2ee459 100644 --- a/packages/sensors/android/build.gradle +++ b/packages/sensors/android/build.gradle @@ -31,4 +31,17 @@ android { lintOptions { disable 'InvalidPackage' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } diff --git a/packages/share/android/build.gradle b/packages/share/android/build.gradle index 231aaa653f2b..1b95bf592fb6 100644 --- a/packages/share/android/build.gradle +++ b/packages/share/android/build.gradle @@ -36,4 +36,17 @@ android { implementation 'androidx.core:core:1.3.1' implementation 'androidx.annotation:annotation:1.1.0' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } diff --git a/packages/shared_preferences/shared_preferences/android/build.gradle b/packages/shared_preferences/shared_preferences/android/build.gradle index 9f7eeca84512..6a66eba508fb 100644 --- a/packages/shared_preferences/shared_preferences/android/build.gradle +++ b/packages/shared_preferences/shared_preferences/android/build.gradle @@ -43,4 +43,17 @@ android { testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-inline:3.9.0' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } diff --git a/packages/url_launcher/url_launcher/android/build.gradle b/packages/url_launcher/url_launcher/android/build.gradle index a0225af4491b..5dd7e773a1ca 100644 --- a/packages/url_launcher/url_launcher/android/build.gradle +++ b/packages/url_launcher/url_launcher/android/build.gradle @@ -31,8 +31,18 @@ android { lintOptions { disable 'InvalidPackage' } + + testOptions { unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } diff --git a/packages/video_player/video_player/android/build.gradle b/packages/video_player/video_player/android/build.gradle index d0ee30375376..f2f18bff9798 100644 --- a/packages/video_player/video_player/android/build.gradle +++ b/packages/video_player/video_player/android/build.gradle @@ -51,4 +51,17 @@ android { testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-inline:3.9.0' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } diff --git a/packages/webview_flutter/webview_flutter/android/build.gradle b/packages/webview_flutter/webview_flutter/android/build.gradle index 41c702f9fc56..cd1b4188a1eb 100644 --- a/packages/webview_flutter/webview_flutter/android/build.gradle +++ b/packages/webview_flutter/webview_flutter/android/build.gradle @@ -40,4 +40,17 @@ android { testImplementation 'org.mockito:mockito-inline:3.11.1' testImplementation 'androidx.test:core:1.3.0' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle b/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle index 8a80e0ce8b6e..2b5a8a7fc209 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle +++ b/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle @@ -30,4 +30,17 @@ android { lintOptions { disable 'InvalidPackage' } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 73a435d83e1d..36b12741f2ce 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -222,7 +222,7 @@ this command. } final int exitCode = await processRunner.runAndStream( - gradleFile.path, ['testDebugUnitTest', '--info'], + gradleFile.path, ['testDebugUnitTest'], workingDir: androidDirectory); if (exitCode != 0) { printError('$exampleName tests failed.'); diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index ca28a6cff0e7..e656e2f23721 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -376,7 +376,7 @@ void main() { orderedEquals([ ProcessCall( androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest', '--info'], + const ['testDebugUnitTest'], androidFolder.path, ), ]), @@ -406,7 +406,7 @@ void main() { orderedEquals([ ProcessCall( androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest', '--info'], + const ['testDebugUnitTest'], androidFolder.path, ), ]), @@ -812,7 +812,7 @@ void main() { orderedEquals([ ProcessCall( androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest', '--info'], + const ['testDebugUnitTest'], androidFolder.path), ProcessCall( 'xcrun', From 1dce6ab475a53cd5ae028a593e89151bfabd617a Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Mon, 26 Jul 2021 11:33:05 -0700 Subject: [PATCH 028/123] Fix webview_flutter Android integration tests and add Espresso (#4147) --- .../example/android/app/build.gradle | 2 +- .../MainActivityTest.java | 0 .../webviewflutterexample/WebViewTest.java | 23 +++++++++ .../android/app/src/debug/AndroidManifest.xml | 17 +++++++ .../android/app/src/main/AndroidManifest.xml | 2 + .../WebViewTestActivity.java | 20 ++++++++ .../webview_flutter_test.dart | 47 +++++++++++++------ .../webview_flutter/example/pubspec.yaml | 1 + .../configs/exclude_integration_android.yaml | 1 - 9 files changed, 96 insertions(+), 17 deletions(-) rename packages/webview_flutter/webview_flutter/example/android/app/src/{androidTestDebug => androidTest}/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java (100%) create mode 100644 packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java create mode 100644 packages/webview_flutter/webview_flutter/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packages/webview_flutter/webview_flutter/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.java diff --git a/packages/webview_flutter/webview_flutter/example/android/app/build.gradle b/packages/webview_flutter/webview_flutter/example/android/app/build.gradle index 47eb97623747..9a43699afb2b 100644 --- a/packages/webview_flutter/webview_flutter/example/android/app/build.gradle +++ b/packages/webview_flutter/webview_flutter/example/android/app/build.gradle @@ -57,6 +57,6 @@ flutter { dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.2.0' } diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java similarity index 100% rename from packages/webview_flutter/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java rename to packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java new file mode 100644 index 000000000000..0b3eeef9b6b7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ActivityScenario; +import io.flutter.plugins.webviewflutter.WebViewFlutterPlugin; +import org.junit.Test; + +public class WebViewTest { + @Test + public void webViewPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(WebViewTestActivity.class); + scenario.onActivity( + activity -> { + assertTrue(activity.engine.getPlugins().has(WebViewFlutterPlugin.class)); + }); + } +} diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/debug/AndroidManifest.xml b/packages/webview_flutter/webview_flutter/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..28792201bc36 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml index 945e47c29e82..e50fcfd9b330 100644 --- a/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml +++ b/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + controllerCompleter = Completer(); await tester.pumpWidget( @@ -36,8 +37,9 @@ void main() { final WebViewController controller = await controllerCompleter.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, 'https://flutter.dev/'); - }); + }, skip: true); + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('loadUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -57,8 +59,9 @@ void main() { await controller.loadUrl('https://www.google.com/'); final String? currentUrl = await controller.currentUrl(); expect(currentUrl, 'https://www.google.com/'); - }); + }, skip: true); + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('loadUrl with headers', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -98,7 +101,7 @@ void main() { final String content = await controller .evaluateJavascript('document.documentElement.innerText'); expect(content.contains('flutter_test_header'), isTrue); - }); + }, skip: Platform.isAndroid); testWidgets('JavaScriptChannel', (WidgetTester tester) async { final Completer controllerCompleter = @@ -274,6 +277,7 @@ void main() { expect(customUserAgent2, 'Custom_User_Agent2'); }); + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('use default platform userAgent after webView is rebuilt', (WidgetTester tester) async { final Completer controllerCompleter = @@ -323,7 +327,7 @@ void main() { final String customUserAgent2 = await _getUserAgent(controller); expect(customUserAgent2, defaultPlatformUserAgent); - }); + }, skip: Platform.isAndroid); group('Video playback policy', () { late String videoTestBase64; @@ -532,6 +536,7 @@ void main() { expect(fullScreen, _webviewBool(false)); }); + // allowsInlineMediaPlayback is a noop on Android, so it is skipped. testWidgets( 'Video plays full screen when allowsInlineMediaPlayback is false', (WidgetTester tester) async { @@ -581,7 +586,7 @@ void main() { String fullScreen = await controller.evaluateJavascript('isFullScreen();'); expect(fullScreen, _webviewBool(true)); - }); + }, skip: Platform.isAndroid); }); group('Audio playback policy', () { @@ -796,6 +801,7 @@ void main() { }); group('Programmatic Scroll', () { + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { final String scrollTestPage = ''' @@ -870,7 +876,7 @@ void main() { scrollPosY = await controller.getScrollY(); expect(scrollPosX, X_SCROLL * 2); expect(scrollPosY, Y_SCROLL * 2); - }); + }, skip: Platform.isAndroid); }); group('SurfaceAndroidWebView', () { @@ -882,6 +888,7 @@ void main() { WebView.platform = null; }); + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { final String scrollTestPage = ''' @@ -948,8 +955,9 @@ void main() { scrollPosY = await controller.getScrollY(); expect(X_SCROLL * 2, scrollPosX); expect(Y_SCROLL * 2, scrollPosY); - }, skip: !Platform.isAndroid); + }, skip: true); + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('inputs are scrolled into view when focused', (WidgetTester tester) async { final String scrollTestPage = ''' @@ -1053,7 +1061,7 @@ void main() { lastInputClientRectRelativeToViewport['right'] <= viewportRectRelativeToViewport['right'], isTrue); - }, skip: !Platform.isAndroid); + }, skip: true); }); group('NavigationDelegate', () { @@ -1272,18 +1280,20 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('window.open("about:blank", "_blank")'); + await controller + .evaluateJavascript('window.open("https://flutter.dev/", "_blank")'); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'about:blank'); + expect(currentUrl, 'https://flutter.dev/'); }); + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets( 'can open new window and go back', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); - final Completer pageLoaded = Completer(); + Completer pageLoaded = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1301,15 +1311,22 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; + expect(controller.currentUrl(), completion('https://flutter.dev/')); + await pageLoaded.future; + pageLoaded = Completer(); + await controller - .evaluateJavascript('window.open("https://www.google.com")'); + .evaluateJavascript('window.open("https://www.google.com/")'); await pageLoaded.future; + pageLoaded = Completer(); expect(controller.currentUrl(), completion('https://www.google.com/')); + expect(controller.canGoBack(), completion(true)); await controller.goBack(); - expect(controller.currentUrl(), completion('https://www.flutter.dev')); + await pageLoaded.future; + expect(controller.currentUrl(), completion('https://flutter.dev/')); }, - skip: !Platform.isAndroid, + skip: true, ); testWidgets( diff --git a/packages/webview_flutter/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/webview_flutter/example/pubspec.yaml index 3529ecc069c8..2316d7941427 100644 --- a/packages/webview_flutter/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/example/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: path: ../ dev_dependencies: + espresso: ^0.1.0+2 flutter_test: sdk: flutter flutter_driver: diff --git a/script/configs/exclude_integration_android.yaml b/script/configs/exclude_integration_android.yaml index 9fc31ec2166a..fc34efa36ac5 100644 --- a/script/configs/exclude_integration_android.yaml +++ b/script/configs/exclude_integration_android.yaml @@ -7,7 +7,6 @@ - shared_preferences/shared_preferences - url_launcher/url_launcher - video_player/video_player -- webview_flutter # Deprecated; no plan to backfill the missing files - android_intent From 6960795eb927196973fed4494129717b12480f0f Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Mon, 26 Jul 2021 15:01:43 -0700 Subject: [PATCH 029/123] Move unit tests to Android modules (#4193) --- .../google_maps_flutter/android/build.gradle | 10 +- .../googlemaps/GoogleMapControllerTest.java | 3 + .../org.mockito.plugins.MockMaker | 1 - .../googlesignin/GoogleSignInTest.java | 175 ++++++++++++++++ .../googlesignin/GoogleSignInPluginTests.java | 189 ------------------ .../image_picker/android/build.gradle | 9 + .../plugins/imagepicker/FileUtilTest.java | 0 .../imagepicker/ImagePickerCacheTest.java | 0 .../imagepicker/ImagePickerDelegateTest.java | 0 .../imagepicker/ImagePickerPluginTest.java | 0 .../plugins/imagepicker/ImageResizerTest.java | 0 .../org.mockito.plugins.MockMaker | 0 .../src/test/resources/pngImage.png | Bin 13 files changed, 191 insertions(+), 196 deletions(-) rename packages/google_maps_flutter/google_maps_flutter/{example/android/app => android}/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java (94%) delete mode 100644 packages/google_maps_flutter/google_maps_flutter/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker delete mode 100644 packages/google_sign_in/google_sign_in/example/android/app/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInPluginTests.java rename packages/image_picker/image_picker/{example/android/app => android}/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java (100%) rename packages/image_picker/image_picker/{example/android/app => android}/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java (100%) rename packages/image_picker/image_picker/{example/android/app => android}/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java (100%) rename packages/image_picker/image_picker/{example/android/app => android}/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java (100%) rename packages/image_picker/image_picker/{example/android/app => android}/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java (100%) rename packages/image_picker/image_picker/{example/android/app => android}/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker (100%) rename packages/image_picker/image_picker/{example/android/app => android}/src/test/resources/pngImage.png (100%) diff --git a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle index 1433d3559b77..6c5ea76ae61e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle @@ -38,6 +38,10 @@ android { androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:3.2.4' + testImplementation 'androidx.test:core:1.2.0' + testImplementation "org.robolectric:robolectric:4.3.1" } compileOptions { @@ -45,7 +49,6 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } - testOptions { unitTests.includeAndroidResources = true unitTests.returnDefaultValues = true @@ -58,8 +61,3 @@ android { } } } - -dependencies { - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:3.2.4' -} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java similarity index 94% rename from packages/google_maps_flutter/google_maps_flutter/example/android/app/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java rename to packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java index 2a81479988e0..6bda085caf46 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java @@ -8,6 +8,7 @@ import static org.junit.Assert.assertTrue; import android.content.Context; +import android.os.Build; import androidx.activity.ComponentActivity; import androidx.test.core.app.ApplicationProvider; import com.google.android.gms.maps.GoogleMap; @@ -19,8 +20,10 @@ import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) +@Config(sdk = Build.VERSION_CODES.P) public class GoogleMapControllerTest { private Context context; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker deleted file mode 100644 index 1f0955d450f0..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +++ /dev/null @@ -1 +0,0 @@ -mock-maker-inline diff --git a/packages/google_sign_in/google_sign_in/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java index 4e7be75aa7cf..3b6ad960f548 100644 --- a/packages/google_sign_in/google_sign_in/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java +++ b/packages/google_sign_in/google_sign_in/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java @@ -5,13 +5,188 @@ package io.flutter.plugins.googlesignin; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.app.Activity; import android.content.Context; +import android.content.Intent; +import com.google.android.gms.auth.api.signin.GoogleSignInAccount; +import com.google.android.gms.common.api.Scope; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; public class GoogleSignInTest { + @Mock Context mockContext; + @Mock Activity mockActivity; + @Mock PluginRegistry.Registrar mockRegistrar; + @Mock BinaryMessenger mockMessenger; + @Spy MethodChannel.Result result; + @Mock GoogleSignInWrapper mockGoogleSignIn; + @Mock GoogleSignInAccount account; + private GoogleSignInPlugin plugin; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mockRegistrar.messenger()).thenReturn(mockMessenger); + when(mockRegistrar.context()).thenReturn(mockContext); + when(mockRegistrar.activity()).thenReturn(mockActivity); + plugin = new GoogleSignInPlugin(); + plugin.initInstance(mockRegistrar.messenger(), mockRegistrar.context(), mockGoogleSignIn); + plugin.setUpRegistrar(mockRegistrar); + } + + @Test + public void requestScopes_ResultErrorIfAccountIsNull() { + MethodCall methodCall = new MethodCall("requestScopes", null); + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); + plugin.onMethodCall(methodCall, result); + verify(result).error("sign_in_required", "No account to grant scopes.", null); + } + + @Test + public void requestScopes_ResultTrueIfAlreadyGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(true); + + plugin.onMethodCall(methodCall, result); + verify(result).success(true); + } + + @Test + public void requestScopes_RequestsPermissionIfNotGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + + verify(mockGoogleSignIn) + .requestPermissions(mockActivity, 53295, account, new Scope[] {requestedScope}); + } + + @Test + public void requestScopes_ReturnsFalseIfPermissionDenied() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, + Activity.RESULT_CANCELED, + new Intent()); + + verify(result).success(false); + } + + @Test + public void requestScopes_ReturnsTrueIfPermissionGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + + verify(result).success(true); + } + + @Test + public void requestScopes_mayBeCalledRepeatedly_ifAlreadyGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + + verify(result, times(2)).success(true); + } + + @Test + public void requestScopes_mayBeCalledRepeatedly_ifNotSignedIn() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + + verify(result, times(2)).error("sign_in_required", "No account to grant scopes.", null); + } + @Test(expected = IllegalStateException.class) public void signInThrowsWithoutActivity() { final GoogleSignInPlugin plugin = new GoogleSignInPlugin(); diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInPluginTests.java b/packages/google_sign_in/google_sign_in/example/android/app/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInPluginTests.java deleted file mode 100644 index f1058760e2de..000000000000 --- a/packages/google_sign_in/google_sign_in/example/android/app/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInPluginTests.java +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.googlesignin; - -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import com.google.android.gms.auth.api.signin.GoogleSignInAccount; -import com.google.android.gms.common.api.Scope; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry; -import io.flutter.plugin.common.PluginRegistry.ActivityResultListener; -import io.flutter.plugins.googlesignin.GoogleSignInPlugin.Delegate; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.Spy; - -public class GoogleSignInPluginTests { - - @Mock Context mockContext; - @Mock Activity mockActivity; - @Mock PluginRegistry.Registrar mockRegistrar; - @Mock BinaryMessenger mockMessenger; - @Spy MethodChannel.Result result; - @Mock GoogleSignInWrapper mockGoogleSignIn; - @Mock GoogleSignInAccount account; - private GoogleSignInPlugin plugin; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - when(mockRegistrar.messenger()).thenReturn(mockMessenger); - when(mockRegistrar.context()).thenReturn(mockContext); - when(mockRegistrar.activity()).thenReturn(mockActivity); - plugin = new GoogleSignInPlugin(); - plugin.initInstance(mockRegistrar.messenger(), mockRegistrar.context(), mockGoogleSignIn); - plugin.setUpRegistrar(mockRegistrar); - } - - @Test - public void requestScopes_ResultErrorIfAccountIsNull() { - MethodCall methodCall = new MethodCall("requestScopes", null); - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); - plugin.onMethodCall(methodCall, result); - verify(result).error("sign_in_required", "No account to grant scopes.", null); - } - - @Test - public void requestScopes_ResultTrueIfAlreadyGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(true); - - plugin.onMethodCall(methodCall, result); - verify(result).success(true); - } - - @Test - public void requestScopes_RequestsPermissionIfNotGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.onMethodCall(methodCall, result); - - verify(mockGoogleSignIn) - .requestPermissions(mockActivity, 53295, account, new Scope[] {requestedScope}); - } - - @Test - public void requestScopes_ReturnsFalseIfPermissionDenied() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(ActivityResultListener.class); - verify(mockRegistrar).addActivityResultListener(captor.capture()); - ActivityResultListener listener = captor.getValue(); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_CANCELED, new Intent()); - - verify(result).success(false); - } - - @Test - public void requestScopes_ReturnsTrueIfPermissionGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(ActivityResultListener.class); - verify(mockRegistrar).addActivityResultListener(captor.capture()); - ActivityResultListener listener = captor.getValue(); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - - verify(result).success(true); - } - - @Test - public void requestScopes_mayBeCalledRepeatedly_ifAlreadyGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(ActivityResultListener.class); - verify(mockRegistrar).addActivityResultListener(captor.capture()); - ActivityResultListener listener = captor.getValue(); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - - verify(result, times(2)).success(true); - } - - @Test - public void requestScopes_mayBeCalledRepeatedly_ifNotSignedIn() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(ActivityResultListener.class); - verify(mockRegistrar).addActivityResultListener(captor.capture()); - ActivityResultListener listener = captor.getValue(); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); - - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - - verify(result, times(2)).error("sign_in_required", "No account to grant scopes.", null); - } -} diff --git a/packages/image_picker/image_picker/android/build.gradle b/packages/image_picker/image_picker/android/build.gradle index e0d51d8dd1f5..607b3c1523a1 100755 --- a/packages/image_picker/image_picker/android/build.gradle +++ b/packages/image_picker/image_picker/android/build.gradle @@ -35,8 +35,17 @@ android { implementation 'androidx.core:core:1.0.2' implementation 'androidx.annotation:annotation:1.0.0' implementation 'androidx.exifinterface:exifinterface:1.3.0' + + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:3.10.0' + testImplementation 'androidx.test:core:1.2.0' + testImplementation "org.robolectric:robolectric:4.3.1" } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } testOptions { unitTests.includeAndroidResources = true diff --git a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java b/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java similarity index 100% rename from packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java rename to packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java diff --git a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java b/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java similarity index 100% rename from packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java rename to packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java diff --git a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java similarity index 100% rename from packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java rename to packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java diff --git a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java similarity index 100% rename from packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java rename to packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java diff --git a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java b/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java similarity index 100% rename from packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java rename to packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java diff --git a/packages/image_picker/image_picker/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/packages/image_picker/image_picker/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker similarity index 100% rename from packages/image_picker/image_picker/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker rename to packages/image_picker/image_picker/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/packages/image_picker/image_picker/example/android/app/src/test/resources/pngImage.png b/packages/image_picker/image_picker/android/src/test/resources/pngImage.png similarity index 100% rename from packages/image_picker/image_picker/example/android/app/src/test/resources/pngImage.png rename to packages/image_picker/image_picker/android/src/test/resources/pngImage.png From 31c598c57571435b4c6c9e6eacd93e6c577d80c8 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 26 Jul 2021 15:04:18 -0700 Subject: [PATCH 030/123] [flutter_plugin_tools] Test and comment Dart analysis (#4194) Adds a unit test and comments intended to avoid accidental breakage of the Dart repo's run of analysis against this repository. Addresses https://github.com/flutter/plugins/pull/4183#issuecomment-885767597 --- .cirrus.yml | 2 ++ script/configs/custom_analysis.yaml | 5 +++ script/tool/test/analyze_command_test.dart | 39 ++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/.cirrus.yml b/.cirrus.yml index 54c4c3799ec3..5e8425fc2437 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -72,6 +72,8 @@ task: - cd script/tool - dart analyze --fatal-infos script: + # DO NOT change the custom-analysis argument here without changing the Dart repo. + # See the comment in script/configs/custom_analysis.yaml for details. - ./script/tool_runner.sh analyze --custom-analysis=script/configs/custom_analysis.yaml ### Android tasks ### - name: build_all_plugins_apk diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml index f6dc8e288b55..2b0f844de7e0 100644 --- a/script/configs/custom_analysis.yaml +++ b/script/configs/custom_analysis.yaml @@ -8,6 +8,11 @@ # from a top-level package into more specific packages in order to incrementally # migrate a federated plugin. # +# DO NOT move or delete this file without updating +# https://github.com/dart-lang/sdk/blob/master/tools/bots/flutter/analyze_flutter_plugins.sh +# which references this file from source, but out-of-repo. +# Contact stuartmorgan or devoncarew for assistance if necessary. + # TODO(ecosystem): Remove everything from this list. See: # https://github.com/flutter/flutter/issues/76229 - camera diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 9dc8b6a3fca5..da2f0aba86c8 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -251,4 +251,43 @@ void main() { ]), ); }); + + // Ensure that the command used to analyze flutter/plugins in the Dart repo: + // https://github.com/dart-lang/sdk/blob/master/tools/bots/flutter/analyze_flutter_plugins.sh + // continues to work. + // + // DO NOT remove or modify this test without a coordination plan in place to + // modify the script above, as it is run from source, but out-of-repo. + // Contact stuartmorgan or devoncarew for assistance. + test('Dart repo analyze command works', () async { + final Directory pluginDir = createFakePlugin('foo', packagesDir, + extraFiles: ['analysis_options.yaml']); + final File allowFile = packagesDir.childFile('custom.yaml'); + allowFile.writeAsStringSync('- foo'); + + await runCapturingPrint(runner, [ + // DO NOT change this call; see comment above. + 'analyze', + '--analysis-sdk', + 'foo/bar/baz', + '--custom-analysis', + allowFile.path + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + const ['packages', 'get'], + pluginDir.path, + ), + ProcessCall( + 'foo/bar/baz/bin/dart', + const ['analyze', '--fatal-infos'], + pluginDir.path, + ), + ]), + ); + }); } From 530a18705f74c4845a45ca20ea0b6d71a36d8e7d Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Tue, 27 Jul 2021 01:18:06 +0200 Subject: [PATCH 031/123] [camera_web] Add `buildPreview` implementation (#4190) --- .../example/integration_test/camera_test.dart | 18 ++++++++++++++++ .../integration_test/camera_web_test.dart | 21 ++++++++++++++++--- .../camera/camera_web/lib/src/camera.dart | 3 +++ .../camera/camera_web/lib/src/camera_web.dart | 4 +++- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 6eeed23ecf56..b92e6e34cc59 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -507,6 +507,24 @@ void main() { }); }); + group('getViewType', () { + testWidgets('returns a correct view type', (tester) async { + const textureId = 1; + + final camera = Camera( + textureId: textureId, + window: window, + ); + + await camera.initialize(); + + expect( + camera.getViewType(), + equals('plugins.flutter.io/camera_$textureId'), + ); + }); + }); + group('dispose', () { testWidgets('resets the video element\'s source', (tester) async { final camera = Camera( diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index d5e1835391ad..7539dd3b33f9 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -11,6 +11,7 @@ import 'package:camera_web/camera_web.dart'; import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_settings.dart'; import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/widgets.dart' as widgets; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -634,10 +635,24 @@ void main() { ); }); - testWidgets('buildPreview throws UnimplementedError', (tester) async { + testWidgets( + 'buildPreview returns an HtmlElementView ' + 'with an appropriate view type', (tester) async { + final camera = Camera( + textureId: cameraId, + window: window, + ); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + expect( - () => CameraPlatform.instance.buildPreview(cameraId), - throwsUnimplementedError, + CameraPlatform.instance.buildPreview(cameraId), + isA().having( + (view) => view.viewType, + 'viewType', + camera.getViewType(), + ), ); }); diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 334f117be274..06551705f056 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -196,6 +196,9 @@ class Camera { } } + /// Returns the registered view type of the camera. + String getViewType() => _getViewType(textureId); + /// Disposes the camera by stopping the camera stream /// and reloading the camera source. void dispose() { diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index e58572e50ee4..263e0539f931 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -361,7 +361,9 @@ class CameraPlugin extends CameraPlatform { @override Widget buildPreview(int cameraId) { - throw UnimplementedError('buildPreview() is not implemented.'); + return HtmlElementView( + viewType: getCamera(cameraId).getViewType(), + ); } @override From 5f42c6920cc6ed3ffe70245e1e840b1969e99be1 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Mon, 26 Jul 2021 22:41:14 -0700 Subject: [PATCH 032/123] Skip an integration test and extend firebase testlab timeout (#4195) --- .../integration_test/webview_flutter_test.dart | 3 ++- script/tool/lib/src/firebase_test_lab_command.dart | 2 +- script/tool/test/firebase_test_lab_command_test.dart | 12 ++++++------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart index 6005cb0a8ba6..876f961a353b 100644 --- a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -103,6 +103,7 @@ void main() { expect(content.contains('flutter_test_header'), isTrue); }, skip: Platform.isAndroid); + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('JavaScriptChannel', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -149,7 +150,7 @@ void main() { // https://github.com/flutter/flutter/issues/66318 await controller.evaluateJavascript('Echo.postMessage("hello");1;'); expect(messagesReceived, equals(['hello'])); - }); + }, skip: Platform.isAndroid); testWidgets('resize webview', (WidgetTester tester) async { final String resizeTest = ''' diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 304912824960..8459f6c70153 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -178,7 +178,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { '--test', 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', '--timeout', - '5m', + '7m', '--results-bucket=${getStringArg('results-bucket')}', '--results-dir=$resultsDir', ]; diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 185b9d83f0fe..35697af3f5fd 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -142,7 +142,7 @@ void main() { '/packages/plugin1/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin1/example'), ProcessCall( @@ -156,7 +156,7 @@ void main() { '/packages/plugin2/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin2/example'), ]), @@ -219,7 +219,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ProcessCall( @@ -229,7 +229,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ]), @@ -445,7 +445,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ]), @@ -601,7 +601,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29' .split(' '), '/packages/plugin/example'), ]), From 2727efc5ba3afcd4f21de2b06a2cbf9955dd41ec Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Tue, 27 Jul 2021 12:46:51 -0700 Subject: [PATCH 033/123] wake lock permission (#4199) --- .../example/android/app/src/main/AndroidManifest.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml index e50fcfd9b330..b8c8d38d45a5 100644 --- a/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml +++ b/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml @@ -34,4 +34,9 @@ to allow setting breakpoints, to provide hot reload, etc. --> + + + From 5516791ec721bc7052a666fa95cafeab19355dfd Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Tue, 27 Jul 2021 13:34:07 -0700 Subject: [PATCH 034/123] [shared_preferences_web] Migrate tests to integration_test (#4185) --- .../shared_preferences_web/CHANGELOG.md | 4 +++ .../shared_preferences_web/example/README.md | 9 +++++++ .../shared_preferences_web_test.dart | 14 ++++++----- .../example/lib/main.dart | 25 +++++++++++++++++++ .../example/pubspec.yaml | 21 ++++++++++++++++ .../example/run_test.sh | 22 ++++++++++++++++ .../example/test_driver/integration_test.dart | 7 ++++++ .../example/web/index.html | 13 ++++++++++ .../shared_preferences_web/test/README.md | 5 ++++ .../test/tests_exist_elsewhere_test.dart | 14 +++++++++++ script/configs/exclude_integration_web.yaml | 2 -- 11 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 packages/shared_preferences/shared_preferences_web/example/README.md rename packages/shared_preferences/shared_preferences_web/{test => example/integration_test}/shared_preferences_web_test.dart (88%) create mode 100644 packages/shared_preferences/shared_preferences_web/example/lib/main.dart create mode 100644 packages/shared_preferences/shared_preferences_web/example/pubspec.yaml create mode 100755 packages/shared_preferences/shared_preferences_web/example/run_test.sh create mode 100644 packages/shared_preferences/shared_preferences_web/example/test_driver/integration_test.dart create mode 100644 packages/shared_preferences/shared_preferences_web/example/web/index.html create mode 100644 packages/shared_preferences/shared_preferences_web/test/README.md create mode 100644 packages/shared_preferences/shared_preferences_web/test/tests_exist_elsewhere_test.dart diff --git a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md index ec08267fe59f..ad5d8f0830fa 100644 --- a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Move tests to `example` directory, so they run as integration_tests with `flutter drive`. + ## 2.0.0 * Migrate to null-safety. diff --git a/packages/shared_preferences/shared_preferences_web/example/README.md b/packages/shared_preferences/shared_preferences_web/example/README.md new file mode 100644 index 000000000000..4348451b14e2 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_web/example/README.md @@ -0,0 +1,9 @@ +# Testing + +This package uses `package:integration_test` to run its tests in a web browser. + +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. + +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. diff --git a/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart b/packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart similarity index 88% rename from packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart rename to packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart index 6e49fb47f755..d95a0512615e 100644 --- a/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart +++ b/packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart @@ -2,11 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@TestOn('chrome') import 'dart:convert' show json; import 'dart:html' as html; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; import 'package:shared_preferences_platform_interface/method_channel_shared_preferences.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:shared_preferences_web/shared_preferences_web.dart'; @@ -20,12 +20,14 @@ const Map kTestValues = { }; void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + group('SharedPreferencesPlugin', () { setUp(() { html.window.localStorage.clear(); }); - test('registers itself', () { + testWidgets('registers itself', (WidgetTester tester) async { SharedPreferencesStorePlatform.instance = MethodChannelSharedPreferencesStore(); expect(SharedPreferencesStorePlatform.instance, @@ -35,7 +37,7 @@ void main() { isA()); }); - test('getAll', () async { + testWidgets('getAll', (WidgetTester tester) async { final SharedPreferencesPlugin store = SharedPreferencesPlugin(); expect(await store.getAll(), isEmpty); @@ -46,7 +48,7 @@ void main() { expect(allData['flutter.testKey'], 'test value'); }); - test('remove', () async { + testWidgets('remove', (WidgetTester tester) async { final SharedPreferencesPlugin store = SharedPreferencesPlugin(); html.window.localStorage['flutter.testKey'] = '"test value"'; expect(html.window.localStorage['flutter.testKey'], isNotNull); @@ -58,7 +60,7 @@ void main() { ); }); - test('setValue', () async { + testWidgets('setValue', (WidgetTester tester) async { final SharedPreferencesPlugin store = SharedPreferencesPlugin(); for (String key in kTestValues.keys) { final dynamic value = kTestValues[key]; @@ -79,7 +81,7 @@ void main() { ); }); - test('clear', () async { + testWidgets('clear', (WidgetTester tester) async { final SharedPreferencesPlugin store = SharedPreferencesPlugin(); html.window.localStorage['flutter.testKey1'] = '"test value"'; html.window.localStorage['flutter.testKey2'] = '42'; diff --git a/packages/shared_preferences/shared_preferences_web/example/lib/main.dart b/packages/shared_preferences/shared_preferences_web/example/lib/main.dart new file mode 100644 index 000000000000..e1a38dcdcd46 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_web/example/lib/main.dart @@ -0,0 +1,25 @@ +// 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:flutter/material.dart'; + +void main() { + runApp(MyApp()); +} + +/// App for testing +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: Text('Testing... Look at the console output for results!'), + ); + } +} diff --git a/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml new file mode 100644 index 000000000000..a83a71b40bf8 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml @@ -0,0 +1,21 @@ +name: shared_preferences_web_integration_tests +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.2.0" + +dependencies: + shared_preferences_web: + path: ../ + flutter: + sdk: flutter + +dev_dependencies: + js: ^0.6.3 + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter diff --git a/packages/shared_preferences/shared_preferences_web/example/run_test.sh b/packages/shared_preferences/shared_preferences_web/example/run_test.sh new file mode 100755 index 000000000000..aa52974f310e --- /dev/null +++ b/packages/shared_preferences/shared_preferences_web/example/run_test.sh @@ -0,0 +1,22 @@ +#!/usr/bin/bash +# 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. + +if pgrep -lf chromedriver > /dev/null; then + echo "chromedriver is running." + + if [ $# -eq 0 ]; then + echo "No target specified, running all tests..." + find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target='{}' + else + echo "Running test target: $1..." + set -x + flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target=$1 + fi + + else + echo "chromedriver is not running." + echo "Please, check the README.md for instructions on how to use run_test.sh" +fi + diff --git a/packages/shared_preferences/shared_preferences_web/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences_web/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_web/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// 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:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/shared_preferences/shared_preferences_web/example/web/index.html b/packages/shared_preferences/shared_preferences_web/example/web/index.html new file mode 100644 index 000000000000..7fb138cc90fa --- /dev/null +++ b/packages/shared_preferences/shared_preferences_web/example/web/index.html @@ -0,0 +1,13 @@ + + + + + + example + + + + + diff --git a/packages/shared_preferences/shared_preferences_web/test/README.md b/packages/shared_preferences/shared_preferences_web/test/README.md new file mode 100644 index 000000000000..7c5b4ad682ba --- /dev/null +++ b/packages/shared_preferences/shared_preferences_web/test/README.md @@ -0,0 +1,5 @@ +## test + +This package uses integration tests for testing. + +See `example/README.md` for more info. diff --git a/packages/shared_preferences/shared_preferences_web/test/tests_exist_elsewhere_test.dart b/packages/shared_preferences/shared_preferences_web/test/tests_exist_elsewhere_test.dart new file mode 100644 index 000000000000..442c50144727 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_web/test/tests_exist_elsewhere_test.dart @@ -0,0 +1,14 @@ +// 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:flutter_test/flutter_test.dart'; + +void main() { + test('Tell the user where to find the real tests', () { + print('---'); + print('This package uses integration_test for its tests.'); + print('See `example/README.md` for more info.'); + print('---'); + }); +} diff --git a/script/configs/exclude_integration_web.yaml b/script/configs/exclude_integration_web.yaml index 99e20831b3c2..6c0fc4efcb7a 100644 --- a/script/configs/exclude_integration_web.yaml +++ b/script/configs/exclude_integration_web.yaml @@ -1,4 +1,2 @@ -# Currently missing: https://github.com/flutter/flutter/issues/81982 -- shared_preferences_web # Currently missing: https://github.com/flutter/flutter/issues/82211 - file_selector From 9e23302ad72f9802776990369a7ee43d3398c60c Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Tue, 27 Jul 2021 23:34:08 +0200 Subject: [PATCH 035/123] [camera_web] Add `takePicture` implementation (#4196) --- .../integration_test/camera_web_test.dart | 38 ++++++++++++++++--- .../integration_test/helpers/mocks.dart | 3 ++ .../camera/camera_web/lib/src/camera_web.dart | 2 +- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 7539dd3b33f9..0b35fcf64234 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -481,11 +481,39 @@ void main() { ); }); - testWidgets('takePicture throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.takePicture(cameraId), - throwsUnimplementedError, - ); + group('takePicture', () { + testWidgets( + 'throws CameraException ' + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notFound, + ), + ), + ); + }); + + testWidgets('captures a picture', (tester) async { + final camera = MockCamera(); + final capturedPicture = MockXFile(); + + when(camera.takePicture) + .thenAnswer((_) => Future.value(capturedPicture)); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final picture = await CameraPlatform.instance.takePicture(cameraId); + + verify(camera.takePicture).called(1); + + expect(picture, equals(capturedPicture)); + }); }); testWidgets('prepareForVideoRecording throws UnimplementedError', diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart index fa627ca0b7e6..54a4f594fe07 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -7,6 +7,7 @@ import 'dart:ui'; import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_settings.dart'; +import 'package:cross_file/cross_file.dart'; import 'package:mocktail/mocktail.dart'; class MockWindow extends Mock implements Window {} @@ -21,6 +22,8 @@ class MockMediaStreamTrack extends Mock implements MediaStreamTrack {} class MockCamera extends Mock implements Camera {} +class MockXFile extends Mock implements XFile {} + /// A fake [MediaStream] that returns the provided [_videoTracks]. class FakeMediaStream extends Fake implements MediaStream { FakeMediaStream(this._videoTracks); diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 263e0539f931..7a5738db3622 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -271,7 +271,7 @@ class CameraPlugin extends CameraPlatform { @override Future takePicture(int cameraId) { - throw UnimplementedError('takePicture() is not implemented.'); + return getCamera(cameraId).takePicture(); } @override From d2f5c33e94291cd21b9999fb02f4e3efb1f618b6 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 27 Jul 2021 14:39:09 -0700 Subject: [PATCH 036/123] Remove pubspec.yaml examples from READMEs (#4198) --- packages/e2e/README.md | 192 ------------------ .../file_selector_web/CHANGELOG.md | 4 + .../file_selector/file_selector_web/README.md | 27 +-- .../file_selector_web/pubspec.yaml | 2 +- .../google_sign_in_web/CHANGELOG.md | 4 + .../google_sign_in_web/README.md | 17 +- .../google_sign_in_web/pubspec.yaml | 2 +- .../image_picker_for_web/CHANGELOG.md | 4 + .../image_picker_for_web/README.md | 18 +- .../image_picker_for_web/pubspec.yaml | 2 +- .../in_app_purchase_android/CHANGELOG.md | 4 + .../in_app_purchase_android/README.md | 34 +--- .../in_app_purchase_android/pubspec.yaml | 2 +- .../in_app_purchase_ios/CHANGELOG.md | 4 + .../in_app_purchase_ios/README.md | 34 +--- .../in_app_purchase_ios/pubspec.yaml | 2 +- .../path_provider_linux/CHANGELOG.md | 4 + .../path_provider_linux/README.md | 9 +- .../path_provider_linux/pubspec.yaml | 2 +- .../path_provider_macos/CHANGELOG.md | 3 +- .../path_provider_macos/README.md | 29 +-- .../path_provider_macos/pubspec.yaml | 2 +- .../path_provider_windows/CHANGELOG.md | 4 + .../path_provider_windows/README.md | 22 +- .../path_provider_windows/pubspec.yaml | 2 +- .../shared_preferences_linux/CHANGELOG.md | 4 + .../shared_preferences_linux/README.md | 21 +- .../shared_preferences_linux/pubspec.yaml | 2 +- .../shared_preferences_macos/CHANGELOG.md | 3 +- .../shared_preferences_macos/README.md | 33 +-- .../shared_preferences_macos/pubspec.yaml | 2 +- .../shared_preferences_web/CHANGELOG.md | 3 +- .../shared_preferences_web/README.md | 31 +-- .../shared_preferences_web/pubspec.yaml | 2 +- .../shared_preferences_windows/CHANGELOG.md | 4 + .../shared_preferences_windows/README.md | 22 +- .../shared_preferences_windows/pubspec.yaml | 2 +- .../url_launcher_linux/CHANGELOG.md | 4 + .../url_launcher/url_launcher_linux/README.md | 33 +-- .../url_launcher_linux/pubspec.yaml | 2 +- .../url_launcher_macos/CHANGELOG.md | 3 +- .../url_launcher/url_launcher_macos/README.md | 33 +-- .../url_launcher_macos/pubspec.yaml | 2 +- .../url_launcher_web/CHANGELOG.md | 4 + .../url_launcher/url_launcher_web/README.md | 36 +--- .../url_launcher_web/pubspec.yaml | 2 +- .../url_launcher_windows/CHANGELOG.md | 4 + .../url_launcher_windows/README.md | 33 +-- .../url_launcher_windows/pubspec.yaml | 2 +- .../video_player_web/CHANGELOG.md | 4 + .../video_player/video_player_web/README.md | 18 +- .../video_player_web/pubspec.yaml | 2 +- 52 files changed, 164 insertions(+), 576 deletions(-) diff --git a/packages/e2e/README.md b/packages/e2e/README.md index 7f211900db70..e86126e4cc56 100644 --- a/packages/e2e/README.md +++ b/packages/e2e/README.md @@ -1,195 +1,3 @@ # e2e (deprecated) -## DEPRECATED - This package has been moved to [integration_test](https://github.com/flutter/plugins/tree/master/packages/integration_test). - -## Old instructions - -This package enables self-driving testing of Flutter code on devices and emulators. -It adapts flutter_test results into a format that is compatible with `flutter drive` -and native Android instrumentation testing. - -## Usage - -Add a dependency on the `e2e` package in the -`dev_dependencies` section of pubspec.yaml. For plugins, do this in the -pubspec.yaml of the example app. - -Invoke `E2EWidgetsFlutterBinding.ensureInitialized()` at the start -of a test file, e.g. - -```dart -import 'package:e2e/e2e.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - testWidgets("failing test example", (WidgetTester tester) async { - expect(2 + 2, equals(5)); - }); -} -``` - -## Test locations - -It is recommended to put e2e tests in the `test/` folder of the app or package. -For example apps, if the e2e test references example app code, it should go in -`example/test/`. It is also acceptable to put e2e tests in `test_driver/` folder -so that they're alongside the runner app (see below). - -## Using Flutter driver to run tests - -`E2EWidgetsTestBinding` supports launching the on-device tests with `flutter drive`. -Note that the tests don't use the `FlutterDriver` API, they use `testWidgets` instead. - -Put the a file named `_e2e_test.dart` in the app' `test_driver` directory: - -```dart -import 'dart:async'; - -import 'package:e2e/e2e_driver.dart' as e2e; - -Future main() async => e2e.main(); - -``` - -To run a example app test with Flutter driver: - -``` -cd example -flutter drive test/_e2e.dart -``` - -To test plugin APIs using Flutter driver: - -``` -cd example -flutter drive --driver=test_driver/_test.dart test/_e2e.dart -``` - -You can run tests on web in release or profile mode. - -First you need to make sure you have downloaded the driver for the browser. - -``` -cd example -flutter drive -v --target=test_driver/dart -d web-server --release --browser-name=chrome -``` - -## Android device testing - -Create an instrumentation test file in your application's -**android/app/src/androidTest/java/com/example/myapp/** directory (replacing -com, example, and myapp with values from your app's package name). You can name -this test file MainActivityTest.java or another name of your choice. - -```java -package com.example.myapp; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -public class MainActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class, true, false); -} -``` - -Update your application's **myapp/android/app/build.gradle** to make sure it -uses androidx's version of AndroidJUnitRunner and has androidx libraries as a -dependency. - -``` -android { - ... - defaultConfig { - ... - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } -} - -dependencies { - testImplementation 'junit:junit:4.12' - - // https://developer.android.com/jetpack/androidx/releases/test/#1.2.0 - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' -} -``` - -To e2e test on a local Android device (emulated or physical): - -``` -./gradlew app:connectedAndroidTest -Ptarget=`pwd`/../test_driver/_e2e.dart -``` - -## Firebase Test Lab - -If this is your first time testing with Firebase Test Lab, you'll need to follow -the guides in the [Firebase test lab -documentation](https://firebase.google.com/docs/test-lab/?gclid=EAIaIQobChMIs5qVwqW25QIV8iCtBh3DrwyUEAAYASAAEgLFU_D_BwE) -to set up a project. - -To run an e2e test on Android devices using Firebase Test Lab, use gradle commands to build an -instrumentation test for Android, after creating `androidTest` as suggested in the last section. - -```bash -pushd android -# flutter build generates files in android/ for building the app -flutter build apk -./gradlew app:assembleAndroidTest -./gradlew app:assembleDebug -Ptarget=.dart -popd -``` - -Upload the build apks Firebase Test Lab, making sure to replace , -, , and with your values. - -```bash -gcloud auth activate-service-account --key-file= -gcloud --quiet config set project -gcloud firebase test android run --type instrumentation \ - --app build/app/outputs/apk/debug/app-debug.apk \ - --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk\ - --timeout 2m \ - --results-bucket= \ - --results-dir= -``` - -You can pass additional parameters on the command line, such as the -devices you want to test on. See -[gcloud firebase test android run](https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run). - -## iOS device testing - -You need to change `iOS/Podfile` to avoid test target statically linking to the plugins. One way is to -link all of the plugins dynamically: - -``` -target 'Runner' do - use_frameworks! - ... -end -``` - -To e2e test on your iOS device (simulator or real), rebuild your iOS targets with Flutter tool. - -``` -flutter build ios -t test_driver/_e2e.dart (--simulator) -``` - -Open Xcode project (by default, it's `ios/Runner.xcodeproj`). Create a test target -(navigating `File > New > Target...` and set up the values) and a test file `RunnerTests.m` and -change the code. You can change `RunnerTests.m` to the name of your choice. - -```objective-c -#import -#import - -E2E_IOS_RUNNER(RunnerTests) -``` - -Now you can start RunnerTests to kick out e2e tests! diff --git a/packages/file_selector/file_selector_web/CHANGELOG.md b/packages/file_selector/file_selector_web/CHANGELOG.md index 3eb7c3b94494..dadf5ffdc3fc 100644 --- a/packages/file_selector/file_selector_web/CHANGELOG.md +++ b/packages/file_selector/file_selector_web/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.8.1+1 + +- Updated installation instructions in README. + # 0.8.1 - Return a non-null value from `getSavePath` for consistency with diff --git a/packages/file_selector/file_selector_web/README.md b/packages/file_selector/file_selector_web/README.md index 24d48f48586f..026e5859e6f3 100644 --- a/packages/file_selector/file_selector_web/README.md +++ b/packages/file_selector/file_selector_web/README.md @@ -1,30 +1,11 @@ -# file_selector_web +# file\_selector\_web The web implementation of [`file_selector`][1]. ## Usage -### Import the package -To use this plugin in your Flutter Web app, simply add it as a dependency in -your pubspec alongside the base `file_selector` plugin. - -_(This is only temporary: in the future we hope to make this package an -"endorsed" implementation of `file_selector`, so that it is automatically -included in your Flutter Web app when you depend on `package:file_selector`.)_ - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - file_selector: ^0.7.0 - file_selector_web: ^0.7.0 - ... -``` - -### Use the plugin -Once you have the `file_selector_web` dependency in your pubspec, you should -be able to use `package:file_selector` as normal. +This package is [endorsed][2], which means you can simply use `file_selector` +normally. This package will be automatically included in your app when you do. [1]: https://pub.dev/packages/file_selector +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/file_selector/file_selector_web/pubspec.yaml b/packages/file_selector/file_selector_web/pubspec.yaml index ebbdfdbbd4da..9753f9216694 100644 --- a/packages/file_selector/file_selector_web/pubspec.yaml +++ b/packages/file_selector/file_selector_web/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_web description: Web platform implementation of file_selector repository: https://github.com/flutter/plugins/tree/master/packages/file_selector/file_selector_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.8.1 +version: 0.8.1+1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md index a5c9e9d2f2bb..8a2f1dbf56d2 100644 --- a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.0+1 + +* Updated installation instructions in README. + ## 0.10.0 * Migrate to null-safety. diff --git a/packages/google_sign_in/google_sign_in_web/README.md b/packages/google_sign_in/google_sign_in_web/README.md index faf04de024af..501ea14eebe6 100644 --- a/packages/google_sign_in/google_sign_in_web/README.md +++ b/packages/google_sign_in/google_sign_in_web/README.md @@ -1,4 +1,4 @@ -# google_sign_in_web +# google\_sign\_in\_web The web implementation of [google_sign_in](https://pub.dev/google_sign_in/google_sign_in) @@ -6,18 +6,9 @@ The web implementation of [google_sign_in](https://pub.dev/google_sign_in/google ### Import the package -This package is the endorsed implementation of `google_sign_in` for the web platform since version `4.1.0`, so it gets automatically added to your dependencies by depending on `google_sign_in: ^4.1.0`. - -No modifications to your pubspec.yaml should be required in a recent enough version of Flutter (`>=1.12.13+hotfix.4`): - -```yaml -... -dependencies: - ... - google_sign_in: ^4.1.0 - ... -... -``` +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), +which means you can simply use `google_sign_in` +normally. This package will be automatically included in your app when you do. ### Web integration diff --git a/packages/google_sign_in/google_sign_in_web/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/pubspec.yaml index 44020fe598c3..0de229e795ce 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android, iOS and Web. repository: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 0.10.0 +version: 0.10.0+1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index f32a5d8e92cd..01d13f900d2d 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.2 + +* Updated installation instructions in README. + # 2.1.1 * Implemented `getMultiImage`. diff --git a/packages/image_picker/image_picker_for_web/README.md b/packages/image_picker/image_picker_for_web/README.md index 8c9f2c73b8fe..73f2dfc4b84f 100644 --- a/packages/image_picker/image_picker_for_web/README.md +++ b/packages/image_picker/image_picker_for_web/README.md @@ -1,4 +1,4 @@ -# image_picker_for_web +# image\_picker\_for\_web A web implementation of [`image_picker`][1]. @@ -52,19 +52,9 @@ The argument `maxDuration` is not supported on the web. ### Import the package -This package is an unendorsed web platform implementation of `image_picker`. - -In order to use this, you'll need to depend in `image_picker: ^0.6.7` (which was the first version of the plugin that allowed federation), and `image_picker_for_web: ^0.1.0`. - -```yaml -... -dependencies: - ... - image_picker: ^0.6.7 - image_picker_for_web: ^0.1.0 - ... -... -``` +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), +which means you can simply use `image_picker` +normally. This package will be automatically included in your app when you do. ### Use the plugin diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index b2479285a3ea..6296992c46d0 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_for_web description: Web platform implementation of image_picker repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 2.1.1 +version: 2.1.2 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index 824b432d5021..32f9aa60e4ca 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.1.4+3 + +- Updated installation instructions in README. + ## 0.1.4+2 * Added price currency symbol to SkuDetailsWrapper. diff --git a/packages/in_app_purchase/in_app_purchase_android/README.md b/packages/in_app_purchase/in_app_purchase_android/README.md index 684dd66d48a2..a2f252f8d3ef 100644 --- a/packages/in_app_purchase/in_app_purchase_android/README.md +++ b/packages/in_app_purchase/in_app_purchase_android/README.md @@ -1,35 +1,15 @@ -# in_app_purchase_android +# in\_app\_purchase\_android The Android implementation of [`in_app_purchase`][1]. ## Usage -### Import the package - -This package has been endorsed, meaning that you only need to add `in_app_purchase` -as a dependency in your `pubspec.yaml`. It will be automatically included in your app -when you depend on `package:in_app_purchase`. - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - in_app_purchase: ^0.6.0 - ... +This package has been [endorsed][2], meaning that you only need to add `in_app_purchase` +as a dependency in your `pubspec.yaml`. This package will be automatically included in your app +when you do. ``` -If you wish to use the Android package only, you can add `in_app_purchase_android` as a -dependency: - -```yaml -... -dependencies: - ... - in_app_purchase_android: ^1.0.0 - ... -``` +If you wish to use the Android package only, you can [add `in_app_purchase_android` directly][3]. ## Contributing @@ -45,4 +25,6 @@ If you would like to contribute to the plugin, check out our [contribution guide](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md). -[1]: ../in_app_purchase/in_app_purchase \ No newline at end of file +[1]: ../in_app_purchase/in_app_purchase +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[3]: https://pub.dev/packages/in_app_purchase_android/install diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml index 41136e7501f6..f8e63821657a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.4+2 +version: 0.1.4+3 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md index 4a2ace891562..305d5a13647c 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.1.3+1 + +- Updated installation instructions in README. + ## 0.1.3 * Add price symbol to platform interface object ProductDetail. diff --git a/packages/in_app_purchase/in_app_purchase_ios/README.md b/packages/in_app_purchase/in_app_purchase_ios/README.md index 46839b5ee3ec..ec72889a8ee2 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/README.md +++ b/packages/in_app_purchase/in_app_purchase_ios/README.md @@ -1,35 +1,15 @@ -# in_app_purchase_ios +# in\_app\_purchase\_ios The iOS implementation of [`in_app_purchase`][1]. ## Usage -### Import the package - -This package has been endorsed, meaning that you only need to add `in_app_purchase` -as a dependency in your `pubspec.yaml`. It will be automatically included in your app -when you depend on `package:in_app_purchase`. - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - in_app_purchase: ^0.6.0 - ... +This package has been [endorsed][2], meaning that you only need to add `in_app_purchase` +as a dependency in your `pubspec.yaml`. This package will be automatically included in your app +when you do. ``` -If you wish to use the iOS package only, you can add `in_app_purchase_ios` as a -dependency: - -```yaml -... -dependencies: - ... - in_app_purchase_ios: ^1.0.0 - ... -``` +If you wish to use the iOS package only, you can [add `in_app_purchase_ios` directly][3]. ## Contributing @@ -45,4 +25,6 @@ If you would like to contribute to the plugin, check out our [contribution guide](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md). -[1]: ../in_app_purchase/in_app_purchase \ No newline at end of file +[1]: ../in_app_purchase/in_app_purchase +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[3]: https://pub.dev/packages/in_app_purchase_ios/install diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index 89b3ad19bacd..5f3b08520eb6 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_ios description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.3 +version: 0.1.3+1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/path_provider/path_provider_linux/CHANGELOG.md b/packages/path_provider/path_provider_linux/CHANGELOG.md index 9383181d6a76..66c11a42c3eb 100644 --- a/packages/path_provider/path_provider_linux/CHANGELOG.md +++ b/packages/path_provider/path_provider_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.2 + +* Updated installation instructions in README. + ## 2.0.1 * Add `implements` to pubspec.yaml. diff --git a/packages/path_provider/path_provider_linux/README.md b/packages/path_provider/path_provider_linux/README.md index ef9e0e855c86..b0b73dcb0ecd 100644 --- a/packages/path_provider/path_provider_linux/README.md +++ b/packages/path_provider/path_provider_linux/README.md @@ -1,8 +1,11 @@ -# path_provider_linux +# path\_provider\_linux The linux implementation of [`path_provider`]. ## Usage -This package is already included as part of the `path_provider` package dependency, and will -be included when using `path_provider` as normal. You will need to use version 1.6.10 or newer. +This package is [endorsed][2], which means you can simply use `path_provider` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/path_provider +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/path_provider/path_provider_linux/pubspec.yaml b/packages/path_provider/path_provider_linux/pubspec.yaml index 7e015dca06db..4d43302ce6b3 100644 --- a/packages/path_provider/path_provider_linux/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_linux description: Linux implementation of the path_provider plugin repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.1 +version: 2.0.2 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/path_provider/path_provider_macos/CHANGELOG.md b/packages/path_provider/path_provider_macos/CHANGELOG.md index d5f9ce860b6f..1d0738c3757a 100644 --- a/packages/path_provider/path_provider_macos/CHANGELOG.md +++ b/packages/path_provider/path_provider_macos/CHANGELOG.md @@ -1,7 +1,8 @@ -## NEXT +# 2.0.2 * Add Swift language version to podspec. * Add native unit tests. +* Updated installation instructions in README. ## 2.0.1 diff --git a/packages/path_provider/path_provider_macos/README.md b/packages/path_provider/path_provider_macos/README.md index 23727fe7f370..00abdf24cd79 100644 --- a/packages/path_provider/path_provider_macos/README.md +++ b/packages/path_provider/path_provider_macos/README.md @@ -1,30 +1,11 @@ -# path_provider_macos +# path\_provider\_macos The macos implementation of [`path_provider`]. ## Usage -### Import the package +This package is [endorsed][2], which means you can simply use `path_provider` +normally. This package will be automatically included in your app when you do. -To use this plugin in your Flutter macos app, simply add it as a dependency in -your `pubspec.yaml` alongside the base `path_provider` plugin. - -_(This is only temporary: in the future we hope to make this package an -"endorsed" implementation of `path_provider`, so that it is automatically -included in your Flutter macos app when you depend on `package:path_provider`.)_ - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - path_provider: ^1.5.1 - path_provider_macos: ^0.0.1 - ... -``` - -### Use the plugin - -Once you have the `path_provider_macos` dependency in your pubspec, you should -be able to use `package:path_provider` as normal. +[1]: https://pub.dev/packages/path_provider +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/path_provider/path_provider_macos/pubspec.yaml b/packages/path_provider/path_provider_macos/pubspec.yaml index 329bffa61c10..140e4cde9d58 100644 --- a/packages/path_provider/path_provider_macos/pubspec.yaml +++ b/packages/path_provider/path_provider_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_macos description: macOS implementation of the path_provider plugin repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.1 +version: 2.0.2 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/path_provider/path_provider_windows/CHANGELOG.md b/packages/path_provider/path_provider_windows/CHANGELOG.md index 2e4da0e1f353..953bb894de09 100644 --- a/packages/path_provider/path_provider_windows/CHANGELOG.md +++ b/packages/path_provider/path_provider_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.3 + +* Updated installation instructions in README. + ## 2.0.2 * Add `implements` to pubspec.yaml. diff --git a/packages/path_provider/path_provider_windows/README.md b/packages/path_provider/path_provider_windows/README.md index 6d452e770469..31813edf21d1 100644 --- a/packages/path_provider/path_provider_windows/README.md +++ b/packages/path_provider/path_provider_windows/README.md @@ -1,23 +1,11 @@ -# path_provider_windows +# path\_provider\_windows The Windows implementation of [`path_provider`][1]. ## Usage -### Import the package +This package is [endorsed][2], which means you can simply use `path_provider` +normally. This package will be automatically included in your app when you do. -This package has been endorsed, meaning that you only need to add `path_provider` -as a dependency in your `pubspec.yaml`. It will be automatically included in your app -when you depend on `package:path_provider`. - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - path_provider: ^1.6.15 - ... -``` - -[1]:../ +[1]: https://pub.dev/packages/path_provider +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/path_provider/path_provider_windows/pubspec.yaml b/packages/path_provider/path_provider_windows/pubspec.yaml index e00e6d1373f2..0353574b6235 100644 --- a/packages/path_provider/path_provider_windows/pubspec.yaml +++ b/packages/path_provider/path_provider_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_windows description: Windows implementation of the path_provider plugin repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.2 +version: 2.0.3 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md index 9a17d2455ad8..fc09bec23591 100644 --- a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.2 + +* Updated installation instructions in README. + ## 2.0.1 * Add `implements` to the pubspec. diff --git a/packages/shared_preferences/shared_preferences_linux/README.md b/packages/shared_preferences/shared_preferences_linux/README.md index 1894f50ae99e..1a4ef3781b7e 100644 --- a/packages/shared_preferences/shared_preferences_linux/README.md +++ b/packages/shared_preferences/shared_preferences_linux/README.md @@ -1,22 +1,11 @@ -# shared_preferences_linux +# shared\_preferences\_linux The Linux implementation of [`shared_preferences`][1]. ## Usage -### Import the package +This package is [endorsed][2], which means you can simply use `shared_preferences` +normally. This package will be automatically included in your app when you do. -This package is an unendorsed Linux implementation of `shared_preferences`. - -In order to use this now, you'll need to depend on `shared_preferences_linux`. -When this package is endorsed it will be automatically used by the `shared_preferences` package and you can switch to that API. - -```yaml -... -dependencies: - ... - shared_preferences_linux: ^0.0.1 - ... -``` - -[1]: ../ +[1]: https://pub.dev/packages/shared_preferences +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml index 9bfe24dfa829..c03e49e042e2 100644 --- a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_linux description: Linux implementation of the shared_preferences plugin repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.1 +version: 2.0.2 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md index d5ace31073ad..2f7e0edf9a51 100644 --- a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 2.0.2 * Add native unit tests. +* Updated installation instructions in README. ## 2.0.1 diff --git a/packages/shared_preferences/shared_preferences_macos/README.md b/packages/shared_preferences/shared_preferences_macos/README.md index 170a8270c402..e9cd7f25be03 100644 --- a/packages/shared_preferences/shared_preferences_macos/README.md +++ b/packages/shared_preferences/shared_preferences_macos/README.md @@ -1,34 +1,11 @@ -# shared_preferences_macos +# shared\_preferences\_macos The macos implementation of [`shared_preferences`][1]. ## Usage -### Import the package +This package is [endorsed][2], which means you can simply use `shared_preferences` +normally. This package will be automatically included in your app when you do. -This package has been endorsed, meaning that you only need to add `shared_preferences` -as a dependency in your `pubspec.yaml`. It will be automatically included in your app -when you depend on `package:shared_preferences`. - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - shared_preferences: ^0.5.6 - ... -``` - -If you wish to use the macos package only, you can add `shared_preferences_macos` as a -dependency: - -```yaml -... -dependencies: - ... - shared_preferences_macos: ^0.0.1 - ... -``` - -[1]: ../ +[1]: https://pub.dev/packages/shared_preferences +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/shared_preferences/shared_preferences_macos/pubspec.yaml b/packages/shared_preferences/shared_preferences_macos/pubspec.yaml index 5eddba2d51ad..6e351e86fb1a 100644 --- a/packages/shared_preferences/shared_preferences_macos/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_macos description: macOS implementation of the shared_preferences plugin. repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.1 +version: 2.0.2 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md index ad5d8f0830fa..0a00e7d66a2a 100644 --- a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.0.1 +* Updated installation instructions in README. * Move tests to `example` directory, so they run as integration_tests with `flutter drive`. ## 2.0.0 diff --git a/packages/shared_preferences/shared_preferences_web/README.md b/packages/shared_preferences/shared_preferences_web/README.md index 8f9d22d86ef5..5c3a51a3d9dc 100644 --- a/packages/shared_preferences/shared_preferences_web/README.md +++ b/packages/shared_preferences/shared_preferences_web/README.md @@ -1,32 +1,11 @@ -# shared_preferences_web +# shared\_preferences\_web The web implementation of [`shared_preferences`][1]. ## Usage -### Import the package +This package is [endorsed][2], which means you can simply use `shared_preferences` +normally. This package will be automatically included in your app when you do. -To use this plugin in your Flutter Web app, simply add it as a dependency in -your `pubspec.yaml` alongside the base `shared_preferences` plugin. - -_(This is only temporary: in the future we hope to make this package an -"endorsed" implementation of `shared_preferences`, so that it is automatically -included in your Flutter Web app when you depend on `package:shared_preferences`.)_ - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - shared_preferences: ^0.5.4+8 - shared_preferences_web: ^0.1.0 - ... -``` - -### Use the plugin - -Once you have the `shared_preferences_web` dependency in your pubspec, you should -be able to use `package:shared_preferences` as normal. - -[1]: ../shared_preferences/shared_preferences +[1]: https://pub.dev/packages/shared_preferences +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/shared_preferences/shared_preferences_web/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/pubspec.yaml index cd2e063fe6b3..2e67be20e427 100644 --- a/packages/shared_preferences/shared_preferences_web/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_web description: Web platform implementation of shared_preferences repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.0 +version: 2.0.1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md index 34c48f37af48..7502ec917d80 100644 --- a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.2 + +* Updated installation instructions in README. + ## 2.0.1 * Add `implements` to pubspec.yaml. diff --git a/packages/shared_preferences/shared_preferences_windows/README.md b/packages/shared_preferences/shared_preferences_windows/README.md index dd710f4c7336..68341acf505e 100644 --- a/packages/shared_preferences/shared_preferences_windows/README.md +++ b/packages/shared_preferences/shared_preferences_windows/README.md @@ -1,23 +1,11 @@ -# shared_preferences_windows +# shared\_preferences\_windows The Windows implementation of [`shared_preferences`][1]. ## Usage -### Import the package +This package is [endorsed][2], which means you can simply use `shared_preferences` +normally. This package will be automatically included in your app when you do. -This package has been endorsed, meaning that you only need to add `shared_preferences` -as a dependency in your `pubspec.yaml`. It will be automatically included in your app -when you depend on `package:shared_preferences`. - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - shared_preferences: ^0.5.7 - ... -``` - -[1]: ../ +[1]: https://pub.dev/packages/shared_preferences +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml index 2cc59d5aa635..87b685f6d0bc 100644 --- a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_windows description: Windows implementation of shared_preferences repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.1 +version: 2.0.2 environment: sdk: '>=2.12.0 <3.0.0' diff --git a/packages/url_launcher/url_launcher_linux/CHANGELOG.md b/packages/url_launcher/url_launcher_linux/CHANGELOG.md index ec9fad53437c..b872a55ef161 100644 --- a/packages/url_launcher/url_launcher_linux/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.1 + +* Updated installation instructions in README. + ## 2.0.0 * Migrate to null safety. diff --git a/packages/url_launcher/url_launcher_linux/README.md b/packages/url_launcher/url_launcher_linux/README.md index 0474c58da40e..23c0019b6948 100644 --- a/packages/url_launcher/url_launcher_linux/README.md +++ b/packages/url_launcher/url_launcher_linux/README.md @@ -1,34 +1,11 @@ -# url_launcher_linux +# url\_launcher\_linux The Linux implementation of [`url_launcher`][1]. ## Usage -### Import the package +This package is [endorsed][2], which means you can simply use `shared_preferences` +normally. This package will be automatically included in your app when you do. -This package has been endorsed, meaning that you only need to add `url_launcher` -as a dependency in your `pubspec.yaml`. It will be automatically included in your app -when you depend on `package:url_launcher`. - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - url_launcher: ^5.5.0 - ... -``` - -If you wish to use the Linux package only, you can add `url_launcher_linux` as a -dependency: - -```yaml -... -dependencies: - ... - url_launcher_linux: ^0.0.1 - ... -``` - -[1]: ../ +[1]: https://pub.dev/packages/url_launcher +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/url_launcher/url_launcher_linux/pubspec.yaml b/packages/url_launcher/url_launcher_linux/pubspec.yaml index a5d6ddd24ff4..e08011e496d5 100644 --- a/packages/url_launcher/url_launcher_linux/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_linux description: Linux implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 2.0.0 +version: 2.0.1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/url_launcher/url_launcher_macos/CHANGELOG.md b/packages/url_launcher/url_launcher_macos/CHANGELOG.md index 976f7719329b..2f672940f2ac 100644 --- a/packages/url_launcher/url_launcher_macos/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_macos/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 2.0.1 * Add native unit tests. +* Updated installation instructions in README. ## 2.0.0 diff --git a/packages/url_launcher/url_launcher_macos/README.md b/packages/url_launcher/url_launcher_macos/README.md index 28aa18817d6c..b594cde1d041 100644 --- a/packages/url_launcher/url_launcher_macos/README.md +++ b/packages/url_launcher/url_launcher_macos/README.md @@ -1,34 +1,11 @@ -# url_launcher_macos +# url\_launcher\_macos The macos implementation of [`url_launcher`][1]. ## Usage -### Import the package +This package is [endorsed][2], which means you can simply use `shared_preferences` +normally. This package will be automatically included in your app when you do. -This package has been endorsed, meaning that you only need to add `url_launcher` -as a dependency in your `pubspec.yaml`. It will be automatically included in your app -when you depend on `package:url_launcher`. - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - url_launcher: ^5.4.1 - ... -``` - -If you wish to use the macos package only, you can add `url_launcher_macos` as a -dependency: - -```yaml -... -dependencies: - ... - url_launcher_macos: ^0.0.1 - ... -``` - -[1]: ../url_launcher/url_launcher +[1]: https://pub.dev/packages/url_launcher +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/url_launcher/url_launcher_macos/pubspec.yaml b/packages/url_launcher/url_launcher_macos/pubspec.yaml index 4a0eac109ab5..2483e35e56de 100644 --- a/packages/url_launcher/url_launcher_macos/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_macos description: macOS implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 2.0.0 +version: 2.0.1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index 488c3387cb68..b1fff136793d 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.2 + +- Updated installation instructions in README. + # 2.0.1 - Change sizing code of `Link` widget's `HtmlElementView` so it works well when slotted. diff --git a/packages/url_launcher/url_launcher_web/README.md b/packages/url_launcher/url_launcher_web/README.md index 21ab2fc52927..b03d15478ee3 100644 --- a/packages/url_launcher/url_launcher_web/README.md +++ b/packages/url_launcher/url_launcher_web/README.md @@ -1,37 +1,11 @@ -# url_launcher_web +# url\_launcher\_web The web implementation of [`url_launcher`][1]. -**Please set your constraint to `url_launcher_web: '>=0.1.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.1.y+z`. -Please use `url_launcher_web: '>=0.1.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage -### Import the package -To use this plugin in your Flutter Web app, simply add it as a dependency in -your pubspec alongside the base `url_launcher` plugin. - -_(This is only temporary: in the future we hope to make this package an -"endorsed" implementation of `url_launcher`, so that it is automatically -included in your Flutter Web app when you depend on `package:url_launcher`.)_ - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - url_launcher: ^5.1.4 - url_launcher_web: ^0.1.0 - ... -``` - -### Use the plugin -Once you have the `url_launcher_web` dependency in your pubspec, you should -be able to use `package:url_launcher` as normal. +This package is [endorsed][2], which means you can simply use `shared_preferences` +normally. This package will be automatically included in your app when you do. -[1]: ../url_launcher +[1]: https://pub.dev/packages/url_launcher +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml index 7afdc0af85e2..dbb658d5fb1f 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_web description: Web platform implementation of url_launcher repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 2.0.1 +version: 2.0.2 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md index e906254eef44..fca798364f6f 100644 --- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.1 + +* Updated installation instructions in README. + ## 2.0.0 * Migrate to null-safety. diff --git a/packages/url_launcher/url_launcher_windows/README.md b/packages/url_launcher/url_launcher_windows/README.md index 4cebb7ed91fb..307f518c4cac 100644 --- a/packages/url_launcher/url_launcher_windows/README.md +++ b/packages/url_launcher/url_launcher_windows/README.md @@ -1,34 +1,11 @@ -# url_launcher_windows +# url\_launcher\_windows The Windows implementation of [`url_launcher`][1]. ## Usage -### Import the package +This package is [endorsed][2], which means you can simply use `shared_preferences` +normally. This package will be automatically included in your app when you do. -This package has been endorsed, meaning that you only need to add `url_launcher` -as a dependency in your `pubspec.yaml`. It will be automatically included in your app -when you depend on `package:url_launcher`. - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - url_launcher: ^5.6.0 - ... -``` - -If you wish to use the Windows package only, you can add `url_launcher_windows` as a -dependency: - -```yaml -... -dependencies: - ... - url_launcher_windows: ^0.0.1 - ... -``` - -[1]: ../url_launcher/url_launcher +[1]: https://pub.dev/packages/url_launcher +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml index 1a82f3e94a43..4d330dd826d5 100644 --- a/packages/url_launcher/url_launcher_windows/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_windows description: Windows implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 2.0.0 +version: 2.0.1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 38bfe90f7b1e..398ec02ba743 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.2 + +* Updated installation instructions in README. + ## 2.0.1 * Fix videos not playing in Safari/Chrome on iOS by setting autoplay to false diff --git a/packages/video_player/video_player_web/README.md b/packages/video_player/video_player_web/README.md index d44f738aeb66..85e55ebcbe80 100644 --- a/packages/video_player/video_player_web/README.md +++ b/packages/video_player/video_player_web/README.md @@ -2,23 +2,11 @@ The web implementation of [`video_player`][1]. - ## Usage -This package is the endorsed implementation of `video_player` for the web platform since version `0.10.5`, so it gets automatically added to your application by depending on `video_player: ^0.10.5`. - -No further modifications to your `pubspec.yaml` should be required in a recent enough version of Flutter (`>=1.12.13+hotfix.4`): - -```yaml -... -dependencies: - ... - video_player: ^0.10.5 - ... -``` - -Once you have the correct `video_player` dependency in your pubspec, you should -be able to use `package:video_player` as normal, even from your web code. +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), +which means you can simply use `video_player` +normally. This package will be automatically included in your app when you do. ## dart:io diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index 568a9262b5f0..f101543598b8 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_web description: Web platform implementation of video_player. repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.0.1 +version: 2.0.2 environment: sdk: ">=2.12.0 <3.0.0" From 08c9b40d020bf249f346322ec395220e36889b42 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Wed, 28 Jul 2021 08:39:06 +0200 Subject: [PATCH 037/123] Expand camera platform interface to support pausing the camera preview. (#4191) --- .../camera_platform_interface/CHANGELOG.md | 4 +++ .../method_channel/method_channel_camera.dart | 16 ++++++++++ .../platform_interface/camera_platform.dart | 10 ++++++ .../camera_platform_interface/pubspec.yaml | 2 +- .../test/camera_platform_interface_test.dart | 26 +++++++++++++++ .../method_channel_camera_test.dart | 32 +++++++++++++++++++ 6 files changed, 89 insertions(+), 1 deletion(-) diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 49214d24d18e..6567d00aa852 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 + +* Introduces interface methods for pausing and resuming the camera preview. + ## 2.0.1 * Update platform_plugin_interface version requirement. diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart index c6c363a56d65..f932f253f491 100644 --- a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -399,6 +399,22 @@ class MethodChannelCamera extends CameraPlatform { } } + @override + Future pausePreview(int cameraId) async { + await _channel.invokeMethod( + 'pausePreview', + {'cameraId': cameraId}, + ); + } + + @override + Future resumePreview(int cameraId) async { + await _channel.invokeMethod( + 'resumePreview', + {'cameraId': cameraId}, + ); + } + @override Widget buildPreview(int cameraId) { return Texture(textureId: cameraId); diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index 4437d3b0593a..9e84e8fdf47c 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -235,6 +235,16 @@ abstract class CameraPlatform extends PlatformInterface { throw UnimplementedError('setZoomLevel() is not implemented.'); } + /// Pause the active preview on the current frame for the selected camera. + Future pausePreview(int cameraId) { + throw UnimplementedError('pausePreview() is not implemented.'); + } + + /// Resume the paused preview for the selected camera. + Future resumePreview(int cameraId) { + throw UnimplementedError('pausePreview() is not implemented.'); + } + /// Returns a widget showing a live camera preview. Widget buildPreview(int cameraId) { throw UnimplementedError('buildView() has not been implemented.'); diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index def06019c268..d691afd41c21 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/camera/camer issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.0.1 +version: 2.1.0 environment: sdk: '>=2.12.0 <3.0.0' diff --git a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart index c8f38efc4e2d..750c27200692 100644 --- a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -408,6 +408,32 @@ void main() { throwsUnimplementedError, ); }); + + test( + 'Default implementation of pausePreview() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.pausePreview(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of resumePreview() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.resumePreview(1), + throwsUnimplementedError, + ); + }); }); } diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart index 8a618545535b..ec71aa173fff 100644 --- a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -923,6 +923,38 @@ void main() { arguments: {'cameraId': cameraId}), ]); }); + + test('Should pause the camera preview', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'pausePreview': null}, + ); + + // Act + await camera.pausePreview(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('pausePreview', arguments: {'cameraId': cameraId}), + ]); + }); + + test('Should resume the camera preview', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'resumePreview': null}, + ); + + // Act + await camera.resumePreview(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('resumePreview', arguments: {'cameraId': cameraId}), + ]); + }); }); }); } From b8786d3ef60169ecf71d7da9264b6727ea726d31 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Wed, 28 Jul 2021 08:44:05 +0200 Subject: [PATCH 038/123] Rebuild camera preview when camera value changes (#4197) --- packages/camera/camera/CHANGELOG.md | 4 +++ .../camera/camera/lib/src/camera_preview.dart | 28 +++++++++++-------- packages/camera/camera/pubspec.yaml | 2 +- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 99156551465d..d455ddb2fad1 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.1+7 + +* Fix device orientation sometimes not affecting the camera preview orientation. + ## 0.8.1+6 * Remove references to the Android V1 embedding. diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index e2f1ff931e42..ad3175a320a9 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -21,17 +21,23 @@ class CameraPreview extends StatelessWidget { @override Widget build(BuildContext context) { return controller.value.isInitialized - ? AspectRatio( - aspectRatio: _isLandscape() - ? controller.value.aspectRatio - : (1 / controller.value.aspectRatio), - child: Stack( - fit: StackFit.expand, - children: [ - _wrapInRotatedBox(child: controller.buildPreview()), - child ?? Container(), - ], - ), + ? ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, child) { + return AspectRatio( + aspectRatio: _isLandscape() + ? controller.value.aspectRatio + : (1 / controller.value.aspectRatio), + child: Stack( + fit: StackFit.expand, + children: [ + _wrapInRotatedBox(child: controller.buildPreview()), + child ?? Container(), + ], + ), + ); + }, + child: child, ) : Container(); } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 56df2cae0151..57161656fc03 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.8.1+6 +version: 0.8.1+7 environment: sdk: ">=2.12.0 <3.0.0" From f506daabf146e43b1e3365c6ed5a229ff97b5828 Mon Sep 17 00:00:00 2001 From: Ludwik Trammer Date: Wed, 28 Jul 2021 20:44:06 +0200 Subject: [PATCH 039/123] [webview_flutter] Better documentation of Android Platform Views modes (#4187) --- .../webview_flutter/CHANGELOG.md | 4 + .../webview_flutter/webview_flutter/README.md | 96 +++++++++++-------- 2 files changed, 62 insertions(+), 38 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 4ffdb08928c2..fcfaf4e5720d 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Improved the documentation on using the different Android Platform View modes. + ## 2.0.11 * Remove references to the Android V1 embedding. diff --git a/packages/webview_flutter/webview_flutter/README.md b/packages/webview_flutter/webview_flutter/README.md index 9a613f5f7a8e..a1a98901affb 100644 --- a/packages/webview_flutter/webview_flutter/README.md +++ b/packages/webview_flutter/webview_flutter/README.md @@ -8,7 +8,7 @@ On iOS the WebView widget is backed by a [WKWebView](https://developer.apple.com On Android the WebView widget is backed by a [WebView](https://developer.android.com/reference/android/webkit/WebView). ## Usage -Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). +Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). If you are targeting Android, make sure to read the *Android Platform Views* section below to choose the platform view mode that best suits your needs. You can now include a WebView widget in your widget tree. See the [WebView](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebView-class.html) @@ -17,58 +17,78 @@ widget's Dartdoc for more details on how to use the widget. ## Android Platform Views The WebView is relying on [Platform Views](https://flutter.dev/docs/development/platform-integration/platform-views) to embed -the Android’s webview within the Flutter app. By default a Virtual Display based platform view -backend is used, this implementation has multiple -[keyboard issues](https://github.com/flutter/flutter/issues?q=is%3Aopen+label%3Avd-only+label%3A%22p%3A+webview-keyboard%22). -When keyboard input is required we recommend using the Hybrid Composition based platform views -implementation. Note that on Android versions prior to Android 10 Hybrid Composition has some -[performance drawbacks](https://flutter.dev/docs/development/platform-integration/platform-views#performance). +the Android’s webview within the Flutter app. It supports two modes: *Virtual displays* (the current default) and *Hybrid composition*. -### Using Hybrid Composition +Here are some points to consider when choosing between the two: + +* *Hybrid composition* mode has a built-in keyboard support while *Virtual displays* mode has multiple +[keyboard issues](https://github.com/flutter/flutter/issues?q=is%3Aopen+label%3Avd-only+label%3A%22p%3A+webview-keyboard%22) +* *Hybrid composition* mode requires Android SKD 19+ while *Virtual displays* mode requires Android SDK 20+ +* *Hybrid composition* mode has [performence limitations](https://flutter.dev/docs/development/platform-integration/platform-views#performance) when working on Android versions prior to Android 10 while *Virtual displays* is performant on all supported Android versions + +| | Hybrid composition | Virtual displays | +| --------------------------- | ------------------- | ---------------- | +| **Full keyboard supoport** | yes | no | +| **Android SDK support** | 19+ | 20+ | +| **Full performance** | Android 10+ | always | +| **The default** | no | yes | + +### Using Virtual displays -1. Set the `minSdkVersion` in `android/app/build.gradle`: +The mode is currently enabled by default. You should however make sure to set the correct `minSdkVersion` in `android/app/build.gradle` (if it was previously lower than 20): ```groovy android { defaultConfig { - minSdkVersion 19 + minSdkVersion 20 } } ``` -This means that app will only be available for users that run Android SDK 19 or higher. -2. To enable hybrid composition, set `WebView.platform = SurfaceAndroidWebView();` in `initState()`. -For example: - -```dart -import 'dart:io'; +### Using Hybrid Composition -import 'package:webview_flutter/webview_flutter.dart'; +1. Set the correct `minSdkVersion` in `android/app/build.gradle` (if it was previously lower than 19): -class WebViewExample extends StatefulWidget { - @override - WebViewExampleState createState() => WebViewExampleState(); -} - -class WebViewExampleState extends State { - @override - void initState() { - super.initState(); - // Enable hybrid composition. + ```groovy + android { + defaultConfig { + minSdkVersion 19 + } + } + ``` + +2. Set `WebView.platform = SurfaceAndroidWebView();` in `initState()`. + For example: + + ```dart + import 'dart:io'; + + import 'package:webview_flutter/webview_flutter.dart'; + + class WebViewExample extends StatefulWidget { + @override + WebViewExampleState createState() => WebViewExampleState(); + } + + class WebViewExampleState extends State { + @override + void initState() { + super.initState(); + // Enable hybrid composition. if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView(); - } - - @override - Widget build(BuildContext context) { - return WebView( - initialUrl: 'https://flutter.dev', - ); - } -} -``` + } + + @override + Widget build(BuildContext context) { + return WebView( + initialUrl: 'https://flutter.dev', + ); + } + } + ``` -#### Enable Material Components for Android +### Enable Material Components for Android To use Material Components when the user interacts with input elements in the WebView, follow the steps described in the [Enabling Material Components instructions](https://flutter.dev/docs/deployment/android#enabling-material-components). From 1b7c4fbce75073042ef1fe9e92ddafa12f1d3b0b Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 28 Jul 2021 20:49:08 +0200 Subject: [PATCH 040/123] Correct mistake in markdown in README.md (#4201) --- packages/in_app_purchase/in_app_purchase_android/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/README.md b/packages/in_app_purchase/in_app_purchase_android/README.md index a2f252f8d3ef..dcf5256e3bbc 100644 --- a/packages/in_app_purchase/in_app_purchase_android/README.md +++ b/packages/in_app_purchase/in_app_purchase_android/README.md @@ -7,7 +7,6 @@ The Android implementation of [`in_app_purchase`][1]. This package has been [endorsed][2], meaning that you only need to add `in_app_purchase` as a dependency in your `pubspec.yaml`. This package will be automatically included in your app when you do. -``` If you wish to use the Android package only, you can [add `in_app_purchase_android` directly][3]. From ab953355ca5987482fce84d5a19a4e6ac5c5f0e0 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 28 Jul 2021 20:54:05 +0200 Subject: [PATCH 041/123] Correct mistake with markdown in README.md (#4202) --- packages/in_app_purchase/in_app_purchase_ios/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/README.md b/packages/in_app_purchase/in_app_purchase_ios/README.md index ec72889a8ee2..fcd4834e9cdc 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/README.md +++ b/packages/in_app_purchase/in_app_purchase_ios/README.md @@ -7,7 +7,6 @@ The iOS implementation of [`in_app_purchase`][1]. This package has been [endorsed][2], meaning that you only need to add `in_app_purchase` as a dependency in your `pubspec.yaml`. This package will be automatically included in your app when you do. -``` If you wish to use the iOS package only, you can [add `in_app_purchase_ios` directly][3]. From ed48cff93b74f4eae4c60e1efd5c648835c9cdf7 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Wed, 28 Jul 2021 20:59:05 +0200 Subject: [PATCH 042/123] [camera_web] Add `dispose` implementation (#4203) --- .../integration_test/camera_web_test.dart | 53 +++++++++++++++++-- .../camera/camera_web/lib/src/camera_web.dart | 5 +- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 0b35fcf64234..c72ce47e1e41 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -684,11 +684,54 @@ void main() { ); }); - testWidgets('dispose throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.dispose(cameraId), - throwsUnimplementedError, - ); + group('dispose', () { + testWidgets( + 'throws CameraException ' + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.dispose(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notFound, + ), + ), + ); + }); + + testWidgets('disposes the correct camera', (tester) async { + const firstCameraId = 0; + const secondCameraId = 1; + + final firstCamera = MockCamera(); + final secondCamera = MockCamera(); + + when(firstCamera.dispose).thenAnswer((_) => Future.value()); + when(secondCamera.dispose).thenAnswer((_) => Future.value()); + + // Save cameras in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras.addAll({ + firstCameraId: firstCamera, + secondCameraId: secondCamera, + }); + + // Dispose the first camera. + await CameraPlatform.instance.dispose(firstCameraId); + + // The first camera should be disposed. + verify(firstCamera.dispose).called(1); + verifyNever(secondCamera.dispose); + + // The first camera should be removed from the camera plugin. + expect( + (CameraPlatform.instance as CameraPlugin).cameras, + equals({ + secondCameraId: secondCamera, + }), + ); + }); }); group('getCamera', () { diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 7a5738db3622..4f130250970c 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -367,8 +367,9 @@ class CameraPlugin extends CameraPlatform { } @override - Future dispose(int cameraId) { - throw UnimplementedError('dispose() is not implemented.'); + Future dispose(int cameraId) async { + getCamera(cameraId).dispose(); + cameras.remove(cameraId); } /// Returns a media video stream for the device with the given [deviceId]. From c8b1d965f6524dc1d58379b49487d466a6df91da Mon Sep 17 00:00:00 2001 From: Yusuf <82844127+ydag@users.noreply.github.com> Date: Thu, 29 Jul 2021 17:04:05 +0200 Subject: [PATCH 043/123] [image_picker] Move UI test (#4181) --- .../image_picker/image_picker/CHANGELOG.md | 4 + .../ios/Runner.xcodeproj/project.pbxproj | 121 +----------------- .../example/ios/RunnerUITestiOS14/Info.plist | 22 ---- .../ImagePickerFromLimitedGalleryUITests.m | 42 +++--- 4 files changed, 25 insertions(+), 164 deletions(-) delete mode 100644 packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/Info.plist rename packages/image_picker/image_picker/example/ios/{RunnerUITestiOS14 => RunnerUITests}/ImagePickerFromLimitedGalleryUITests.m (76%) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index fef3e47cdf1a..e7048c371a95 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Move `ImagePickerFromLimitedGalleryUITests` to `RunnerUITests` target. + ## 0.8.2 * Added new methods that return `package:cross_file` `XFile` instances. [Docs](https://pub.dev/documentation/cross_file/latest/index.html). diff --git a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj index fc1609f5eeda..2f28c9ad2d6d 100644 --- a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj @@ -25,7 +25,7 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 9FC8F0E9229FA49E00C8D58F /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; 9FC8F0EC229FA68500C8D58F /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; - BE7AEE7926403CC8006181AA /* ImagePickerFromLimitedGalleryUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = BE7AEE7826403CC8006181AA /* ImagePickerFromLimitedGalleryUITests.m */; }; + BE6173D826A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */; }; F4F7A436CCA4BF276270A3AE /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EC32F6993F4529982D9519F1 /* libPods-Runner.a */; }; /* End PBXBuildFile section */ @@ -44,13 +44,6 @@ remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; - BE7AEE7126403C46006181AA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -100,7 +93,7 @@ 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = gifImage.gif; sourceTree = ""; }; 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImageUtilTests.m; sourceTree = ""; }; A908FAEEA2A9B26D903C09C5 /* libPods-RunnerUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - BE7AEE6C26403C46006181AA /* RunnerUITestiOS14.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITestiOS14.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerFromLimitedGalleryUITests.m; sourceTree = ""; }; BE7AEE7026403C46006181AA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; BE7AEE7826403CC8006181AA /* ImagePickerFromLimitedGalleryUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerFromLimitedGalleryUITests.m; sourceTree = ""; }; DC6FCAAD4E7580C9B3C2E21D /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; @@ -134,13 +127,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - BE7AEE6926403C46006181AA /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -171,6 +157,7 @@ 6801C8372555D726009DAF8D /* RunnerUITests */ = { isa = PBXGroup; children = ( + BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */, 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */, 6801C83A2555D726009DAF8D /* Info.plist */, ); @@ -221,7 +208,6 @@ children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 6801C8362555D726009DAF8D /* RunnerUITests.xctest */, - BE7AEE6C26403C46006181AA /* RunnerUITestiOS14.xctest */, 334733F22668136400DCC49E /* RunnerTests.xctest */, ); name = Products; @@ -332,24 +318,6 @@ productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; - BE7AEE6B26403C46006181AA /* RunnerUITestiOS14 */ = { - isa = PBXNativeTarget; - buildConfigurationList = BE7AEE7526403C46006181AA /* Build configuration list for PBXNativeTarget "RunnerUITestiOS14" */; - buildPhases = ( - BE7AEE6826403C46006181AA /* Sources */, - BE7AEE6926403C46006181AA /* Frameworks */, - BE7AEE6A26403C46006181AA /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - BE7AEE7226403C46006181AA /* PBXTargetDependency */, - ); - name = RunnerUITestiOS14; - productName = RunnerUITestiOS14; - productReference = BE7AEE6C26403C46006181AA /* RunnerUITestiOS14.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -378,11 +346,6 @@ }; }; }; - BE7AEE6B26403C46006181AA = { - CreatedOnToolsVersion = 12.4; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; - }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -401,7 +364,6 @@ 97C146ED1CF9000F007C117D /* Runner */, 334733F12668136400DCC49E /* RunnerTests */, 6801C8352555D726009DAF8D /* RunnerUITests */, - BE7AEE6B26403C46006181AA /* RunnerUITestiOS14 */, ); }; /* End PBXProject section */ @@ -435,13 +397,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - BE7AEE6A26403C46006181AA /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -555,6 +510,7 @@ buildActionMask = 2147483647; files = ( 6801C8392555D726009DAF8D /* ImagePickerFromGalleryUITests.m in Sources */, + BE6173D826A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -568,14 +524,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - BE7AEE6826403C46006181AA /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - BE7AEE7926403CC8006181AA /* ImagePickerFromLimitedGalleryUITests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -589,11 +537,6 @@ target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 6801C83B2555D726009DAF8D /* PBXContainerItemProxy */; }; - BE7AEE7226403C46006181AA /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = BE7AEE7126403C46006181AA /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -843,53 +786,6 @@ }; name = Release; }; - BE7AEE7326403C46006181AA /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerUITestiOS14/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.baseflow.RunnerUITestiOS14; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Runner; - }; - name = Debug; - }; - BE7AEE7426403C46006181AA /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerUITestiOS14/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.baseflow.RunnerUITestiOS14; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Runner; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -929,15 +825,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - BE7AEE7526403C46006181AA /* Build configuration list for PBXNativeTarget "RunnerUITestiOS14" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - BE7AEE7326403C46006181AA /* Debug */, - BE7AEE7426403C46006181AA /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/Info.plist b/packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/Info.plist deleted file mode 100644 index 64d65ca49577..000000000000 --- a/packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/ImagePickerFromLimitedGalleryUITests.m b/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m similarity index 76% rename from packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/ImagePickerFromLimitedGalleryUITests.m rename to packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m index 86cad03d27cf..802a494b0f5e 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/ImagePickerFromLimitedGalleryUITests.m +++ b/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m @@ -25,31 +25,18 @@ - (void)setUp { __weak typeof(self) weakSelf = self; [self addUIInterruptionMonitorWithDescription:@"Permission popups" handler:^BOOL(XCUIElement* _Nonnull interruptingElement) { - if (@available(iOS 14, *)) { - XCUIElement* limitedPhotoPermission = - [interruptingElement.buttons elementBoundByIndex:0]; - if (![limitedPhotoPermission - waitForExistenceWithTimeout: - kLimitedElementWaitingTime]) { - os_log_error(OS_LOG_DEFAULT, "%@", - weakSelf.app.debugDescription); - XCTFail(@"Failed due to not able to find " - @"selectPhotos butt on with %@ seconds", - @(kLimitedElementWaitingTime)); - } - [limitedPhotoPermission tap]; - } else { - XCUIElement* ok = interruptingElement.buttons[@"OK"]; - if (![ok waitForExistenceWithTimeout: - kLimitedElementWaitingTime]) { - os_log_error(OS_LOG_DEFAULT, "%@", - weakSelf.app.debugDescription); - XCTFail(@"Failed due to not able to find ok button " - @"with %@ seconds", - @(kLimitedElementWaitingTime)); - } - [ok tap]; + XCUIElement* limitedPhotoPermission = + [interruptingElement.buttons elementBoundByIndex:0]; + if (![limitedPhotoPermission + waitForExistenceWithTimeout: + kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", + weakSelf.app.debugDescription); + XCTFail(@"Failed due to not able to find " + @"selectPhotos button with %@ seconds", + @(kLimitedElementWaitingTime)); } + [limitedPhotoPermission tap]; return YES; }]; } @@ -60,7 +47,12 @@ - (void)tearDown { } - (void)testSelectingFromGallery { - [self launchPickerAndSelect]; + // Test the `Select Photos` button which is available after iOS 14. + if (@available(iOS 14, *)) { + [self launchPickerAndSelect]; + } else { + return; + } } - (void)launchPickerAndSelect { From 97fa266174f8f6101aed1857315d6f4cfc81571c Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Fri, 30 Jul 2021 10:14:05 -0700 Subject: [PATCH 044/123] Adds test harnesses for integration tests on Android (#4200) --- .../example/android/app/build.gradle | 9 ++++---- .../googlemapsexample/GoogleMapsTest.java | 23 +++++++++++++++++++ .../MainActivityTest.java | 0 .../android/app/src/debug/AndroidManifest.xml | 17 ++++++++++++++ .../GoogleMapsTestActivity.java | 20 ++++++++++++++++ .../google_maps_flutter/example/pubspec.yaml | 1 + .../example/android/app/build.gradle | 5 +++- .../FlutterActivityTest.java | 0 .../googlesigninexample/GoogleSignInTest.java | 23 +++++++++++++++++++ .../android/app/src/debug/AndroidManifest.xml | 17 ++++++++++++++ .../GoogleSignInTestActivity.java | 20 ++++++++++++++++ .../org.mockito.plugins.MockMaker | 1 - .../google_sign_in/example/pubspec.yaml | 1 + .../example/android/app/build.gradle | 9 ++++---- .../FlutterActivityTest.java | 0 .../imagepickerexample/ImagePickerTest.java | 23 +++++++++++++++++++ .../android/app/src/debug/AndroidManifest.xml | 17 ++++++++++++++ .../ImagePickerTestActivity.java | 20 ++++++++++++++++ ...icker_test.dart => image_picker_test.dart} | 3 +++ .../image_picker/example/pubspec.yaml | 1 + .../example/android/app/build.gradle | 5 ++-- .../FlutterActivityTest.java | 0 .../quickactionsexample/QuickActionsTest.java | 23 +++++++++++++++++++ .../android/app/src/debug/AndroidManifest.xml | 17 ++++++++++++++ .../QuickActionsTestActivity.java | 20 ++++++++++++++++ .../quick_actions/example/pubspec.yaml | 1 + .../configs/exclude_integration_android.yaml | 3 --- 27 files changed, 262 insertions(+), 17 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java rename packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/{googlemaps => googlemapsexample}/MainActivityTest.java (100%) create mode 100644 packages/google_maps_flutter/google_maps_flutter/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packages/google_maps_flutter/google_maps_flutter/example/android/app/src/debug/java/io/flutter/plugins/googlemapsexample/GoogleMapsTestActivity.java rename packages/google_sign_in/google_sign_in/example/android/app/src/{main => androidTest}/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java (100%) create mode 100644 packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/GoogleSignInTest.java create mode 100644 packages/google_sign_in/google_sign_in/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/GoogleSignInTestActivity.java delete mode 100644 packages/google_sign_in/google_sign_in/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker rename packages/image_picker/image_picker/example/android/app/src/{main => androidTest}/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java (100%) create mode 100644 packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerTest.java create mode 100644 packages/image_picker/image_picker/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/ImagePickerTestActivity.java rename packages/image_picker/image_picker/example/integration_test/{old_image_picker_test.dart => image_picker_test.dart} (71%) rename packages/quick_actions/quick_actions/example/android/app/src/{main => androidTest}/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java (100%) create mode 100644 packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java create mode 100644 packages/quick_actions/quick_actions/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/QuickActionsTestActivity.java diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/build.gradle b/packages/google_maps_flutter/google_maps_flutter/example/android/app/build.gradle index 1a8cdf52cc46..d850810db651 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/build.gradle @@ -35,6 +35,7 @@ android { applicationId "io.flutter.plugins.googlemapsexample" minSdkVersion 20 targetSdkVersion 28 + multiDexEnabled true versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -60,11 +61,9 @@ android { dependencies { testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' - testImplementation 'androidx.test:core:1.2.0' - testImplementation "org.robolectric:robolectric:4.3.1" - testImplementation 'org.mockito:mockito-core:3.2.4' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.2.0' testImplementation 'com.google.android.gms:play-services-maps:17.0.0' } } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java new file mode 100644 index 000000000000..40552ddf7be1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemapsexample; + +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ActivityScenario; +import io.flutter.plugins.googlemaps.GoogleMapsPlugin; +import org.junit.Test; + +public class GoogleMapsTest { + @Test + public void googleMapsPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(GoogleMapsTestActivity.class); + scenario.onActivity( + activity -> { + assertTrue(activity.engine.getPlugins().has(GoogleMapsPlugin.class)); + }); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/MainActivityTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/MainActivityTest.java rename to packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/debug/AndroidManifest.xml b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..9c1f83d3cec5 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/debug/java/io/flutter/plugins/googlemapsexample/GoogleMapsTestActivity.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/debug/java/io/flutter/plugins/googlemapsexample/GoogleMapsTestActivity.java new file mode 100644 index 000000000000..e183a7c75c4e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/debug/java/io/flutter/plugins/googlemapsexample/GoogleMapsTestActivity.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemapsexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Makes the FlutterEngine accessible for testing. +public class GoogleMapsTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml index 74135b31e8d7..d15f76352b69 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: flutter_plugin_android_lifecycle: ^2.0.1 dev_dependencies: + espresso: ^0.1.0+2 flutter_driver: sdk: flutter integration_test: diff --git a/packages/google_sign_in/google_sign_in/example/android/app/build.gradle b/packages/google_sign_in/google_sign_in/example/android/app/build.gradle index 2952c3b9c463..5d574a2c6a51 100644 --- a/packages/google_sign_in/google_sign_in/example/android/app/build.gradle +++ b/packages/google_sign_in/google_sign_in/example/android/app/build.gradle @@ -35,6 +35,7 @@ android { applicationId "io.flutter.plugins.googlesigninexample" minSdkVersion 16 targetSdkVersion 28 + multiDexEnabled true versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -60,5 +61,7 @@ flutter { dependencies { implementation 'com.google.android.gms:play-services-auth:16.0.1' testImplementation'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.17.0' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.2.0' } diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java similarity index 100% rename from packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java rename to packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/GoogleSignInTest.java new file mode 100644 index 000000000000..561d9d4e7a82 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/GoogleSignInTest.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlesigninexample; + +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ActivityScenario; +import io.flutter.plugins.googlesignin.GoogleSignInPlugin; +import org.junit.Test; + +public class GoogleSignInTest { + @Test + public void googleSignInPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(GoogleSignInTestActivity.class); + scenario.onActivity( + activity -> { + assertTrue(activity.engine.getPlugins().has(GoogleSignInPlugin.class)); + }); + } +} diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/debug/AndroidManifest.xml b/packages/google_sign_in/google_sign_in/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..4d764900a530 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/GoogleSignInTestActivity.java b/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/GoogleSignInTestActivity.java new file mode 100644 index 000000000000..09506a2632df --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/GoogleSignInTestActivity.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlesigninexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Makes the FlutterEngine accessible for testing. +public class GoogleSignInTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/packages/google_sign_in/google_sign_in/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker deleted file mode 100644 index 1f0955d450f0..000000000000 --- a/packages/google_sign_in/google_sign_in/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +++ /dev/null @@ -1 +0,0 @@ -mock-maker-inline diff --git a/packages/google_sign_in/google_sign_in/example/pubspec.yaml b/packages/google_sign_in/google_sign_in/example/pubspec.yaml index 8ecfbb6c4369..0379b9065333 100644 --- a/packages/google_sign_in/google_sign_in/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/example/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: http: ^0.13.0 dev_dependencies: + espresso: ^0.1.0+2 pedantic: ^1.10.0 integration_test: sdk: flutter diff --git a/packages/image_picker/image_picker/example/android/app/build.gradle b/packages/image_picker/image_picker/example/android/app/build.gradle index cc77d33eed0d..f7fbaae4c9fd 100755 --- a/packages/image_picker/image_picker/example/android/app/build.gradle +++ b/packages/image_picker/image_picker/example/android/app/build.gradle @@ -36,6 +36,7 @@ android { applicationId "io.flutter.plugins.imagepicker.example" minSdkVersion 16 targetSdkVersion 28 + multiDexEnabled true versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -60,9 +61,7 @@ flutter { dependencies { testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:3.10.0' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' - testImplementation 'androidx.test:core:1.2.0' - testImplementation "org.robolectric:robolectric:4.3.1" + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.2.0' } diff --git a/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java similarity index 100% rename from packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java rename to packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java diff --git a/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerTest.java b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerTest.java new file mode 100644 index 000000000000..c4a1532d940c --- /dev/null +++ b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerTest.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.imagepickerexample; + +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ActivityScenario; +import io.flutter.plugins.imagepicker.ImagePickerPlugin; +import org.junit.Test; + +public class ImagePickerTest { + @Test + public void imagePickerPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(ImagePickerTestActivity.class); + scenario.onActivity( + activity -> { + assertTrue(activity.engine.getPlugins().has(ImagePickerPlugin.class)); + }); + } +} diff --git a/packages/image_picker/image_picker/example/android/app/src/debug/AndroidManifest.xml b/packages/image_picker/image_picker/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..6f85cefded34 --- /dev/null +++ b/packages/image_picker/image_picker/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/ImagePickerTestActivity.java b/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/ImagePickerTestActivity.java new file mode 100644 index 000000000000..827687a10e79 --- /dev/null +++ b/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/ImagePickerTestActivity.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.imagepickerexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Makes the FlutterEngine accessible for testing. +public class ImagePickerTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/image_picker/image_picker/example/integration_test/old_image_picker_test.dart b/packages/image_picker/image_picker/example/integration_test/image_picker_test.dart similarity index 71% rename from packages/image_picker/image_picker/example/integration_test/old_image_picker_test.dart rename to packages/image_picker/image_picker/example/integration_test/image_picker_test.dart index 120c9e221c24..2b82b4bda5e4 100644 --- a/packages/image_picker/image_picker/example/integration_test/old_image_picker_test.dart +++ b/packages/image_picker/image_picker/example/integration_test/image_picker_test.dart @@ -2,8 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('placeholder test', (WidgetTester tester) async {}); } diff --git a/packages/image_picker/image_picker/example/pubspec.yaml b/packages/image_picker/image_picker/example/pubspec.yaml index 422bd5a4120d..44ae0fc22c06 100755 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: path: ../ dev_dependencies: + espresso: ^0.1.0+2 flutter_driver: sdk: flutter integration_test: diff --git a/packages/quick_actions/quick_actions/example/android/app/build.gradle b/packages/quick_actions/quick_actions/example/android/app/build.gradle index 57de7f6e5e03..485ae5511063 100644 --- a/packages/quick_actions/quick_actions/example/android/app/build.gradle +++ b/packages/quick_actions/quick_actions/example/android/app/build.gradle @@ -53,6 +53,7 @@ flutter { dependencies { testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.2.0' } diff --git a/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java similarity index 100% rename from packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java rename to packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java diff --git a/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java new file mode 100644 index 000000000000..9d2fed13fc27 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.quickactionsexample; + +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ActivityScenario; +import io.flutter.plugins.quickactions.QuickActionsPlugin; +import org.junit.Test; + +public class QuickActionsTest { + @Test + public void imagePickerPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(QuickActionsTestActivity.class); + scenario.onActivity( + activity -> { + assertTrue(activity.engine.getPlugins().has(QuickActionsPlugin.class)); + }); + } +} diff --git a/packages/quick_actions/quick_actions/example/android/app/src/debug/AndroidManifest.xml b/packages/quick_actions/quick_actions/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..bee689df1735 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/QuickActionsTestActivity.java b/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/QuickActionsTestActivity.java new file mode 100644 index 000000000000..4ff3a27cd5c0 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/QuickActionsTestActivity.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.quickactionsexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Makes the FlutterEngine accessible for testing. +public class QuickActionsTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/quick_actions/quick_actions/example/pubspec.yaml b/packages/quick_actions/quick_actions/example/pubspec.yaml index eaf3de4b56e0..c4ee86039761 100644 --- a/packages/quick_actions/quick_actions/example/pubspec.yaml +++ b/packages/quick_actions/quick_actions/example/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: path: ../ dev_dependencies: + espresso: ^0.1.0+2 flutter_driver: sdk: flutter integration_test: diff --git a/script/configs/exclude_integration_android.yaml b/script/configs/exclude_integration_android.yaml index fc34efa36ac5..d8bd10b3a36e 100644 --- a/script/configs/exclude_integration_android.yaml +++ b/script/configs/exclude_integration_android.yaml @@ -1,9 +1,7 @@ # Currently missing harness files: https://github.com/flutter/flutter/issues/86749) - camera/camera -- google_sign_in/google_sign_in - in_app_purchase/in_app_purchase - in_app_purchase_android -- quick_actions - shared_preferences/shared_preferences - url_launcher/url_launcher - video_player/video_player @@ -17,5 +15,4 @@ - wifi_info_flutter/wifi_info_flutter # No integration tests to run: -- image_picker/image_picker - espresso From 477a13d8b0af92aea5e8e53445b1bfd0ef28f32b Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 30 Jul 2021 21:29:04 +0200 Subject: [PATCH 045/123] [camera_web] Add camera errors handling (#4207) --- .../camera_settings_test.dart | 320 ++++++++- .../example/integration_test/camera_test.dart | 408 +++--------- .../integration_test/camera_web_test.dart | 619 ++++++++++++++---- .../integration_test/helpers/mocks.dart | 53 +- .../camera/camera_web/lib/src/camera.dart | 98 +-- .../camera_web/lib/src/camera_settings.dart | 104 ++- .../camera/camera_web/lib/src/camera_web.dart | 380 +++++++---- .../lib/src/types/camera_error_code.dart | 71 ++ .../lib/src/types/camera_error_codes.dart | 32 - .../lib/src/types/camera_web_exception.dart | 29 + .../camera_web/lib/src/types/types.dart | 3 +- .../camera_web/test/helpers/helpers.dart | 5 + .../camera/camera_web/test/helpers/mocks.dart | 17 + .../test/types/camera_error_code_test.dart | 133 ++++ .../test/types/camera_web_exception_test.dart | 35 + 15 files changed, 1596 insertions(+), 711 deletions(-) create mode 100644 packages/camera/camera_web/lib/src/types/camera_error_code.dart delete mode 100644 packages/camera/camera_web/lib/src/types/camera_error_codes.dart create mode 100644 packages/camera/camera_web/lib/src/types/camera_web_exception.dart create mode 100644 packages/camera/camera_web/test/helpers/helpers.dart create mode 100644 packages/camera/camera_web/test/helpers/mocks.dart create mode 100644 packages/camera/camera_web/test/types/camera_error_code_test.dart create mode 100644 packages/camera/camera_web/test/types/camera_web_exception_test.dart diff --git a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart index ddfb86e4ec0a..7e5119003129 100644 --- a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart @@ -8,6 +8,7 @@ import 'dart:ui'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera_settings.dart'; import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -18,6 +19,8 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('CameraSettings', () { + const cameraId = 0; + late Window window; late Navigator navigator; late MediaDevices mediaDevices; @@ -34,9 +37,314 @@ void main() { settings = CameraSettings()..window = window; }); + group('getMediaStreamForOptions', () { + testWidgets( + 'calls MediaDevices.getUserMedia ' + 'with provided options', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenAnswer((_) async => FakeMediaStream([])); + + final options = CameraOptions( + video: VideoConstraints( + facingMode: FacingModeConstraint.exact(CameraType.user), + width: VideoSizeConstraint(ideal: 200), + ), + ); + + await settings.getMediaStreamForOptions(options); + + verify( + () => mediaDevices.getUserMedia(options.toJson()), + ).called(1); + }); + + testWidgets( + 'throws PlatformException ' + 'with notSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + expect( + () => settings.getMediaStreamForOptions(CameraOptions()), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notSupported.toString(), + ), + ), + ); + }); + + group('throws CameraWebException', () { + testWidgets( + 'with notFound error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with NotFoundError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('NotFoundError')); + + expect( + () => settings.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.notFound), + ), + ); + }); + + testWidgets( + 'with notFound error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with DevicesNotFoundError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('DevicesNotFoundError')); + + expect( + () => settings.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.notFound), + ), + ); + }); + + testWidgets( + 'with notReadable error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with NotReadableError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('NotReadableError')); + + expect( + () => settings.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.notReadable), + ), + ); + }); + + testWidgets( + 'with notReadable error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with TrackStartError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('TrackStartError')); + + expect( + () => settings.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.notReadable), + ), + ); + }); + + testWidgets( + 'with overconstrained error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with OverconstrainedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('OverconstrainedError')); + + expect( + () => settings.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having( + (e) => e.code, 'code', CameraErrorCode.overconstrained), + ), + ); + }); + + testWidgets( + 'with overconstrained error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with ConstraintNotSatisfiedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('ConstraintNotSatisfiedError')); + + expect( + () => settings.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having( + (e) => e.code, 'code', CameraErrorCode.overconstrained), + ), + ); + }); + + testWidgets( + 'with permissionDenied error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with NotAllowedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('NotAllowedError')); + + expect( + () => settings.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having( + (e) => e.code, 'code', CameraErrorCode.permissionDenied), + ), + ); + }); + + testWidgets( + 'with permissionDenied error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with PermissionDeniedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('PermissionDeniedError')); + + expect( + () => settings.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having( + (e) => e.code, 'code', CameraErrorCode.permissionDenied), + ), + ); + }); + + testWidgets( + 'with type error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with TypeError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('TypeError')); + + expect( + () => settings.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.type), + ), + ); + }); + + testWidgets( + 'with abort error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with AbortError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('AbortError')); + + expect( + () => settings.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.abort), + ), + ); + }); + + testWidgets( + 'with security error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with SecurityError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('SecurityError')); + + expect( + () => settings.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.security), + ), + ); + }); + + testWidgets( + 'with unknown error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with an unknown error', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('Unknown')); + + expect( + () => settings.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.unknown), + ), + ); + }); + + testWidgets( + 'with unknown error ' + 'when MediaDevices.getUserMedia throws an unknown exception', + (tester) async { + when(() => mediaDevices.getUserMedia(any())).thenThrow(Exception()); + + expect( + () => settings.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.unknown), + ), + ); + }); + }); + }); + group('getFacingModeForVideoTrack', () { testWidgets( - 'throws CameraException ' + 'throws PlatformException ' 'with notSupported error ' 'when there are no media devices', (tester) async { when(() => navigator.mediaDevices).thenReturn(null); @@ -44,10 +352,10 @@ void main() { expect( () => settings.getFacingModeForVideoTrack(MockMediaStreamTrack()), throwsA( - isA().having( + isA().having( (e) => e.code, 'code', - CameraErrorCodes.notSupported, + CameraErrorCode.notSupported.toString(), ), ), ); @@ -145,7 +453,7 @@ void main() { }); testWidgets( - 'throws CameraException ' + 'throws PlatformException ' 'with unknown error ' 'when getting the video track capabilities ' 'throws an unknown error', (tester) async { @@ -157,10 +465,10 @@ void main() { expect( () => settings.getFacingModeForVideoTrack(videoTrack), throwsA( - isA().having( + isA().having( (e) => e.code, 'code', - CameraErrorCodes.unknown, + CameraErrorCode.unknown.toString(), ), ), ); diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index b92e6e34cc59..49690ed38ab5 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -5,10 +5,9 @@ import 'dart:html'; import 'dart:ui'; -import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; -import 'package:camera_web/src/types/camera_error_codes.dart'; -import 'package:camera_web/src/types/camera_options.dart'; +import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/types/types.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -19,27 +18,54 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('Camera', () { - late Window window; - late Navigator navigator; late MediaStream mediaStream; - late MediaDevices mediaDevices; + late CameraSettings cameraSettings; setUp(() { - window = MockWindow(); - navigator = MockNavigator(); - mediaDevices = MockMediaDevices(); + cameraSettings = MockCameraSettings(); final videoElement = getVideoElementWithBlankStream(Size(10, 10)); mediaStream = videoElement.captureStream(); - when(() => window.navigator).thenReturn(navigator); - when(() => navigator.mediaDevices).thenReturn(mediaDevices); when( - () => mediaDevices.getUserMedia(any()), - ).thenAnswer((_) async => mediaStream); + () => cameraSettings.getMediaStreamForOptions( + any(), + cameraId: any(named: 'cameraId'), + ), + ).thenAnswer((_) => Future.value(mediaStream)); + }); + + setUpAll(() { + registerFallbackValue(MockCameraOptions()); }); group('initialize', () { + testWidgets( + 'calls CameraSettings.getMediaStreamForOptions ' + 'with provided options', (tester) async { + final options = CameraOptions( + video: VideoConstraints( + facingMode: FacingModeConstraint.exact(CameraType.user), + width: VideoSizeConstraint(ideal: 200), + ), + ); + + final camera = Camera( + textureId: 1, + options: options, + cameraSettings: cameraSettings, + ); + + await camera.initialize(); + + verify( + () => cameraSettings.getMediaStreamForOptions( + options, + cameraId: 1, + ), + ).called(1); + }); + testWidgets( 'creates a video element ' 'with correct properties', (tester) async { @@ -50,7 +76,7 @@ void main() { options: CameraOptions( audio: audioConstraints, ), - window: window, + cameraSettings: cameraSettings, ); await camera.initialize(); @@ -75,7 +101,7 @@ void main() { 'with correct properties', (tester) async { final camera = Camera( textureId: 1, - window: window, + cameraSettings: cameraSettings, ); await camera.initialize(); @@ -85,311 +111,24 @@ void main() { expect(camera.divElement.children, contains(camera.videoElement)); }); - testWidgets('calls getUserMedia with provided options', (tester) async { - final options = CameraOptions( - video: VideoConstraints( - facingMode: FacingModeConstraint.exact(CameraType.user), - width: VideoSizeConstraint(ideal: 200), - ), - ); + testWidgets( + 'throws an exception ' + 'when CameraSettings.getMediaStreamForOptions throws', + (tester) async { + final exception = Exception('A media stream exception occured.'); - final optionsJson = await options.toJson(); + when(() => cameraSettings.getMediaStreamForOptions(any(), + cameraId: any(named: 'cameraId'))).thenThrow(exception); final camera = Camera( textureId: 1, - options: options, - window: window, + cameraSettings: cameraSettings, ); - await camera.initialize(); - - verify(() => mediaDevices.getUserMedia(optionsJson)).called(1); - }); - - group('throws CameraException', () { - testWidgets( - 'with notSupported error ' - 'when there are no media devices', (tester) async { - when(() => navigator.mediaDevices).thenReturn(null); - - final camera = Camera( - textureId: 1, - window: window, - ); - - expect( - camera.initialize, - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.notSupported, - ), - ), - ); - }); - - testWidgets( - 'with notFound error ' - 'when getUserMedia throws DomException ' - 'with NotFoundError', (tester) async { - when(() => mediaDevices.getUserMedia(any())) - .thenThrow(FakeDomException('NotFoundError')); - - final camera = Camera( - textureId: 1, - window: window, - ); - - expect( - camera.initialize, - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.notFound, - ), - ), - ); - }); - - testWidgets( - 'with notFound error ' - 'when getUserMedia throws DomException ' - 'with DevicesNotFoundError', (tester) async { - when(() => mediaDevices.getUserMedia(any())) - .thenThrow(FakeDomException('DevicesNotFoundError')); - - final camera = Camera( - textureId: 1, - window: window, - ); - - expect( - camera.initialize, - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.notFound, - ), - ), - ); - }); - - testWidgets( - 'with notReadable error ' - 'when getUserMedia throws DomException ' - 'with NotReadableError', (tester) async { - when(() => mediaDevices.getUserMedia(any())) - .thenThrow(FakeDomException('NotReadableError')); - - final camera = Camera( - textureId: 1, - window: window, - ); - - expect( - camera.initialize, - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.notReadable, - ), - ), - ); - }); - - testWidgets( - 'with notReadable error ' - 'when getUserMedia throws DomException ' - 'with TrackStartError', (tester) async { - when(() => mediaDevices.getUserMedia(any())) - .thenThrow(FakeDomException('TrackStartError')); - - final camera = Camera( - textureId: 1, - window: window, - ); - - expect( - camera.initialize, - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.notReadable, - ), - ), - ); - }); - - testWidgets( - 'with overconstrained error ' - 'when getUserMedia throws DomException ' - 'with OverconstrainedError', (tester) async { - when(() => mediaDevices.getUserMedia(any())) - .thenThrow(FakeDomException('OverconstrainedError')); - - final camera = Camera( - textureId: 1, - window: window, - ); - - expect( - camera.initialize, - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.overconstrained, - ), - ), - ); - }); - - testWidgets( - 'with overconstrained error ' - 'when getUserMedia throws DomException ' - 'with ConstraintNotSatisfiedError', (tester) async { - when(() => mediaDevices.getUserMedia(any())) - .thenThrow(FakeDomException('ConstraintNotSatisfiedError')); - - final camera = Camera( - textureId: 1, - window: window, - ); - - expect( - camera.initialize, - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.overconstrained, - ), - ), - ); - }); - - testWidgets( - 'with permissionDenied error ' - 'when getUserMedia throws DomException ' - 'with NotAllowedError', (tester) async { - when(() => mediaDevices.getUserMedia(any())) - .thenThrow(FakeDomException('NotAllowedError')); - - final camera = Camera( - textureId: 1, - window: window, - ); - - expect( - camera.initialize, - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.permissionDenied, - ), - ), - ); - }); - - testWidgets( - 'with permissionDenied error ' - 'when getUserMedia throws DomException ' - 'with PermissionDeniedError', (tester) async { - when(() => mediaDevices.getUserMedia(any())) - .thenThrow(FakeDomException('PermissionDeniedError')); - - final camera = Camera( - textureId: 1, - window: window, - ); - - expect( - camera.initialize, - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.permissionDenied, - ), - ), - ); - }); - - testWidgets( - 'with type error ' - 'when getUserMedia throws DomException ' - 'with TypeError', (tester) async { - when(() => mediaDevices.getUserMedia(any())) - .thenThrow(FakeDomException('TypeError')); - - final camera = Camera( - textureId: 1, - window: window, - ); - - expect( - camera.initialize, - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.type, - ), - ), - ); - }); - - testWidgets( - 'with unknown error ' - 'when getUserMedia throws DomException ' - 'with an unknown error', (tester) async { - when(() => mediaDevices.getUserMedia(any())) - .thenThrow(FakeDomException('Unknown')); - - final camera = Camera( - textureId: 1, - window: window, - ); - - expect( - camera.initialize, - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.unknown, - ), - ), - ); - }); - - testWidgets( - 'with unknown error ' - 'when getUserMedia throws an unknown exception', (tester) async { - when(() => mediaDevices.getUserMedia(any())).thenThrow(Exception()); - - final camera = Camera( - textureId: 1, - window: window, - ); - - expect( - camera.initialize, - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.unknown, - ), - ), - ); - }); + expect( + camera.initialize, + throwsA(exception), + ); }); }); @@ -399,35 +138,54 @@ void main() { final camera = Camera( textureId: 1, - window: window, + cameraSettings: cameraSettings, ); await camera.initialize(); - camera.videoElement.onPlay.listen((event) => startedPlaying = true); + final cameraPlaySubscription = + camera.videoElement.onPlay.listen((event) => startedPlaying = true); await camera.play(); expect(startedPlaying, isTrue); + + await cameraPlaySubscription.cancel(); }); testWidgets( - 'assigns media stream to the video element\'s source ' + 'assigns a media stream ' + 'from CameraSettings.getMediaStreamForOptions ' + 'to the video element\'s source ' 'if it does not exist', (tester) async { + final options = CameraOptions( + video: VideoConstraints( + width: VideoSizeConstraint(ideal: 100), + ), + ); + final camera = Camera( textureId: 1, - window: window, + options: options, + cameraSettings: cameraSettings, ); await camera.initialize(); /// Remove the video element's source /// by stopping the camera. - // ignore: cascade_invocations camera.stop(); await camera.play(); + // Should be called twice: for initialize and play. + verify( + () => cameraSettings.getMediaStreamForOptions( + options, + cameraId: 1, + ), + ).called(2); + expect(camera.videoElement.srcObject, mediaStream); }); }); @@ -436,7 +194,7 @@ void main() { testWidgets('resets the video element\'s source', (tester) async { final camera = Camera( textureId: 1, - window: window, + cameraSettings: cameraSettings, ); await camera.initialize(); @@ -452,7 +210,7 @@ void main() { testWidgets('returns a captured picture', (tester) async { final camera = Camera( textureId: 1, - window: window, + cameraSettings: cameraSettings, ); await camera.initialize(); @@ -475,7 +233,7 @@ void main() { final camera = Camera( textureId: 1, - window: window, + cameraSettings: cameraSettings, ); await camera.initialize(); @@ -495,7 +253,7 @@ void main() { final camera = Camera( textureId: 1, - window: window, + cameraSettings: cameraSettings, ); await camera.initialize(); @@ -513,7 +271,7 @@ void main() { final camera = Camera( textureId: textureId, - window: window, + cameraSettings: cameraSettings, ); await camera.initialize(); @@ -529,7 +287,7 @@ void main() { testWidgets('resets the video element\'s source', (tester) async { final camera = Camera( textureId: 1, - window: window, + cameraSettings: cameraSettings, ); await camera.initialize(); diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index c72ce47e1e41..1b540a50e48d 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:html'; import 'dart:ui'; @@ -42,9 +43,15 @@ void main() { when(() => window.navigator).thenReturn(navigator); when(() => navigator.mediaDevices).thenReturn(mediaDevices); + when( - () => mediaDevices.getUserMedia(any()), - ).thenAnswer((_) async => videoElement.captureStream()); + () => cameraSettings.getMediaStreamForOptions( + any(), + cameraId: any(named: 'cameraId'), + ), + ).thenAnswer( + (_) async => videoElement.captureStream(), + ); CameraPlatform.instance = CameraPlugin( cameraSettings: cameraSettings, @@ -53,6 +60,7 @@ void main() { setUpAll(() { registerFallbackValue(MockMediaStreamTrack()); + registerFallbackValue(MockCameraOptions()); }); testWidgets('CameraPlugin is the live instance', (tester) async { @@ -66,29 +74,27 @@ void main() { any(), ), ).thenReturn(null); + + when(mediaDevices.enumerateDevices).thenAnswer( + (_) async => [], + ); }); - testWidgets( - 'throws CameraException ' - 'with notSupported error ' - 'when there are no media devices', (tester) async { - when(() => navigator.mediaDevices).thenReturn(null); + testWidgets('requests video and audio permissions', (tester) async { + final _ = await CameraPlatform.instance.availableCameras(); - expect( - () => CameraPlatform.instance.availableCameras(), - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.notSupported, + verify( + () => cameraSettings.getMediaStreamForOptions( + CameraOptions( + audio: AudioConstraints(enabled: true), ), ), - ); + ).called(1); }); testWidgets( - 'calls MediaDevices.getUserMedia ' - 'on the video input device', (tester) async { + 'gets a video stream ' + 'for a video input device', (tester) async { final videoDevice = FakeMediaDeviceInfo( '1', 'Camera 1', @@ -102,19 +108,47 @@ void main() { final _ = await CameraPlatform.instance.availableCameras(); verify( - () => mediaDevices.getUserMedia( + () => cameraSettings.getMediaStreamForOptions( CameraOptions( video: VideoConstraints( deviceId: videoDevice.deviceId, ), - ).toJson(), + ), ), ).called(1); }); testWidgets( - 'calls CameraSettings.getLensDirectionForVideoTrack ' - 'on the first video track of the video input device', (tester) async { + 'does not get a video stream ' + 'for the video input device ' + 'with an empty device id', (tester) async { + final videoDevice = FakeMediaDeviceInfo( + '', + 'Camera 1', + MediaDeviceKind.videoInput, + ); + + when(mediaDevices.enumerateDevices).thenAnswer( + (_) => Future.value([videoDevice]), + ); + + final _ = await CameraPlatform.instance.availableCameras(); + + verifyNever( + () => cameraSettings.getMediaStreamForOptions( + CameraOptions( + video: VideoConstraints( + deviceId: videoDevice.deviceId, + ), + ), + ), + ); + }); + + testWidgets( + 'gets the facing mode ' + 'from the first available video track ' + 'of the video input device', (tester) async { final videoDevice = FakeMediaDeviceInfo( '1', 'Camera 1', @@ -125,10 +159,10 @@ void main() { FakeMediaStream([MockMediaStreamTrack(), MockMediaStreamTrack()]); when( - () => mediaDevices.getUserMedia( + () => cameraSettings.getMediaStreamForOptions( CameraOptions( video: VideoConstraints(deviceId: videoDevice.deviceId), - ).toJson(), + ), ), ).thenAnswer((_) => Future.value(videoStream)); @@ -147,7 +181,8 @@ void main() { testWidgets( 'returns appropriate camera descriptions ' - 'for multiple media devices', (tester) async { + 'for multiple video devices ' + 'based on video streams', (tester) async { final firstVideoDevice = FakeMediaDeviceInfo( '1', 'Camera 1', @@ -174,35 +209,35 @@ void main() { firstVideoDevice, FakeMediaDeviceInfo( '2', - 'Camera 2', + 'Audio Input 2', MediaDeviceKind.audioInput, ), FakeMediaDeviceInfo( '3', - 'Camera 3', + 'Audio Output 3', MediaDeviceKind.audioOutput, ), secondVideoDevice, ]), ); - // Mock media devices to return the first video stream + // Mock camera settings to return the first video stream // for the first video device. when( - () => mediaDevices.getUserMedia( + () => cameraSettings.getMediaStreamForOptions( CameraOptions( video: VideoConstraints(deviceId: firstVideoDevice.deviceId), - ).toJson(), + ), ), ).thenAnswer((_) => Future.value(firstVideoStream)); - // Mock media devices to return the second video stream + // Mock camera settings to return the second video stream // for the second video device. when( - () => mediaDevices.getUserMedia( + () => cameraSettings.getMediaStreamForOptions( CameraOptions( video: VideoConstraints(deviceId: secondVideoDevice.deviceId), - ).toJson(), + ), ), ).thenAnswer((_) => Future.value(secondVideoStream)); @@ -265,10 +300,10 @@ void main() { ); when( - () => mediaDevices.getUserMedia( + () => cameraSettings.getMediaStreamForOptions( CameraOptions( video: VideoConstraints(deviceId: videoDevice.deviceId), - ).toJson(), + ), ), ).thenAnswer((_) => Future.value(videoStream)); @@ -293,33 +328,93 @@ void main() { }), ); }); - }); - group('createCamera', () { - testWidgets( - 'throws CameraException ' - 'with missingMetadata error ' - 'if there is no metadata ' - 'for the given camera description', (tester) async { - expect( - () => CameraPlatform.instance.createCamera( - CameraDescription( - name: 'name', - lensDirection: CameraLensDirection.back, - sensorOrientation: 0, + group('throws CameraException', () { + testWidgets( + 'with notSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + expect( + () => CameraPlatform.instance.availableCameras(), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notSupported.toString(), + ), ), - ResolutionPreset.ultraHigh, - ), - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.missingMetadata, + ); + }); + + testWidgets('when MediaDevices.enumerateDevices throws DomException', + (tester) async { + final exception = FakeDomException(DomException.UNKNOWN); + + when(mediaDevices.enumerateDevices).thenThrow(exception); + + expect( + () => CameraPlatform.instance.availableCameras(), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), ), - ), - ); + ); + }); + + testWidgets( + 'when CameraSettings.getMediaStreamForOptions ' + 'throws CameraWebException', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.security, + 'description', + ); + + when(() => cameraSettings.getMediaStreamForOptions(any())) + .thenThrow(exception); + + expect( + () => CameraPlatform.instance.availableCameras(), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + + testWidgets( + 'when CameraSettings.getMediaStreamForOptions ' + 'throws PlatformException', (tester) async { + final exception = PlatformException( + code: CameraErrorCode.notSupported.toString(), + message: 'message', + ); + + when(() => cameraSettings.getMediaStreamForOptions(any())) + .thenThrow(exception); + + expect( + () => CameraPlatform.instance.availableCameras(), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); }); + }); + group('createCamera', () { group('creates a camera', () { const ultraHighResolutionSize = Size(3840, 2160); const maxResolutionSize = Size(3840, 2160); @@ -365,11 +460,6 @@ void main() { 'textureId', cameraId, ) - .having( - (camera) => camera.window, - 'window', - window, - ) .having( (camera) => camera.options, 'options', @@ -426,29 +516,51 @@ void main() { ); }); }); - }); - group('initializeCamera', () { testWidgets( 'throws CameraException ' - 'with notFound error ' - 'if the camera does not exist', (tester) async { + 'with missingMetadata error ' + 'if there is no metadata ' + 'for the given camera description', (tester) async { expect( - () => CameraPlatform.instance.initializeCamera(cameraId), + () => CameraPlatform.instance.createCamera( + CameraDescription( + name: 'name', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.ultraHigh, + ), throwsA( isA().having( (e) => e.code, 'code', - CameraErrorCodes.notFound, + CameraErrorCode.missingMetadata.toString(), ), ), ); }); + }); - testWidgets('initializes and plays the camera', (tester) async { - final camera = MockCamera(); + group('initializeCamera', () { + late Camera camera; + late VideoElement videoElement; - when(camera.getVideoSize).thenAnswer((_) => Future.value(Size(10, 10))); + setUp(() { + camera = MockCamera(); + videoElement = MockVideoElement(); + + when(() => camera.videoElement).thenReturn(videoElement); + when(() => videoElement.onError) + .thenAnswer((_) => FakeElementStream(Stream.empty())); + when(() => videoElement.onAbort) + .thenAnswer((_) => FakeElementStream(Stream.empty())); + }); + + testWidgets('initializes and plays the camera', (tester) async { + when(camera.getVideoSize).thenAnswer( + (_) => Future.value(Size(10, 10)), + ); when(camera.initialize).thenAnswer((_) => Future.value()); when(camera.play).thenAnswer((_) => Future.value()); @@ -460,6 +572,68 @@ void main() { verify(camera.initialize).called(1); verify(camera.play).called(1); }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when camera throws CameraWebException', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.permissionDenied, + 'description', + ); + + when(camera.initialize).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + + testWidgets('when camera throws DomException', (tester) async { + final exception = FakeDomException(DomException.NOT_ALLOWED); + + when(camera.initialize).thenAnswer((_) => Future.value()); + when(camera.play).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name.toString(), + ), + ), + ); + }); + }); }); testWidgets('lockCaptureOrientation throws UnimplementedError', @@ -482,22 +656,6 @@ void main() { }); group('takePicture', () { - testWidgets( - 'throws CameraException ' - 'with notFound error ' - 'if the camera does not exist', (tester) async { - expect( - () => CameraPlatform.instance.initializeCamera(cameraId), - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.notFound, - ), - ), - ); - }); - testWidgets('captures a picture', (tester) async { final camera = MockCamera(); final capturedPicture = MockXFile(); @@ -514,6 +672,44 @@ void main() { expect(picture, equals(capturedPicture)); }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when takePicture throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(camera.takePicture).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); }); testWidgets('prepareForVideoRecording throws UnimplementedError', @@ -668,7 +864,7 @@ void main() { 'with an appropriate view type', (tester) async { final camera = Camera( textureId: cameraId, - window: window, + cameraSettings: cameraSettings, ); // Save the camera in the camera plugin. @@ -685,22 +881,6 @@ void main() { }); group('dispose', () { - testWidgets( - 'throws CameraException ' - 'with notFound error ' - 'if the camera does not exist', (tester) async { - expect( - () => CameraPlatform.instance.dispose(cameraId), - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCodes.notFound, - ), - ), - ); - }); - testWidgets('disposes the correct camera', (tester) async { const firstCameraId = 0; const secondCameraId = 1; @@ -732,11 +912,86 @@ void main() { }), ); }); + + testWidgets('cancels camera video and abort error subscriptions', + (tester) async { + final camera = MockCamera(); + final videoElement = MockVideoElement(); + + final errorStreamController = StreamController(); + final abortStreamController = StreamController(); + + when(camera.getVideoSize).thenAnswer( + (_) => Future.value(Size(10, 10)), + ); + when(camera.initialize).thenAnswer((_) => Future.value()); + when(camera.play).thenAnswer((_) => Future.value()); + + when(() => camera.videoElement).thenReturn(videoElement); + when(() => videoElement.onError) + .thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when(() => videoElement.onAbort) + .thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + + expect(errorStreamController.hasListener, isTrue); + expect(abortStreamController.hasListener, isTrue); + + await CameraPlatform.instance.dispose(cameraId); + + expect(errorStreamController.hasListener, isFalse); + expect(abortStreamController.hasListener, isFalse); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.dispose(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when dispose throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.INVALID_ACCESS); + + when(camera.dispose).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.dispose(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); }); group('getCamera', () { testWidgets('returns the correct camera', (tester) async { - final camera = Camera(textureId: cameraId, window: window); + final camera = Camera( + textureId: cameraId, + cameraSettings: cameraSettings, + ); // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -748,16 +1003,16 @@ void main() { }); testWidgets( - 'throws CameraException ' + 'throws PlatformException ' 'with notFound error ' 'if the camera does not exist', (tester) async { expect( () => (CameraPlatform.instance as CameraPlugin).getCamera(cameraId), throwsA( - isA().having( + isA().having( (e) => e.code, 'code', - CameraErrorCodes.notFound, + CameraErrorCode.notFound.toString(), ), ), ); @@ -774,12 +1029,15 @@ void main() { videoElement = getVideoElementWithBlankStream(videoSize); when( - () => mediaDevices.getUserMedia(any()), + () => cameraSettings.getMediaStreamForOptions( + any(), + cameraId: cameraId, + ), ).thenAnswer((_) async => videoElement.captureStream()); final camera = Camera( textureId: cameraId, - window: window, + cameraSettings: cameraSettings, ); // Save the camera in the camera plugin. @@ -794,14 +1052,16 @@ void main() { expect( await streamQueue.next, - CameraInitializedEvent( - cameraId, - videoSize.width, - videoSize.height, - ExposureMode.auto, - false, - FocusMode.auto, - false, + equals( + CameraInitializedEvent( + cameraId, + videoSize.width, + videoSize.height, + ExposureMode.auto, + false, + FocusMode.auto, + false, + ), ), ); @@ -823,11 +1083,126 @@ void main() { ); }); - testWidgets('onCameraError throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.onCameraError(cameraId), - throwsUnimplementedError, - ); + group('onCameraError', () { + late Camera camera; + late VideoElement videoElement; + + late StreamController errorStreamController, + abortStreamController; + + setUp(() { + camera = MockCamera(); + videoElement = MockVideoElement(); + + errorStreamController = StreamController(); + abortStreamController = StreamController(); + + when(camera.getVideoSize).thenAnswer( + (_) => Future.value(Size(10, 10)), + ); + when(camera.initialize).thenAnswer((_) => Future.value()); + when(camera.play).thenAnswer((_) => Future.value()); + + when(() => camera.videoElement).thenReturn(videoElement); + when(() => videoElement.onError).thenAnswer( + (_) => FakeElementStream(errorStreamController.stream)); + when(() => videoElement.onAbort).thenAnswer( + (_) => FakeElementStream(abortStreamController.stream)); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on video error ' + 'with a message', (tester) async { + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + final error = FakeMediaError( + MediaError.MEDIA_ERR_NETWORK, + 'A network error occured.', + ); + + final errorCode = CameraErrorCode.fromMediaError(error); + + when(() => videoElement.error).thenReturn(error); + + errorStreamController.add(Event('error')); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${errorCode}, error message: ${error.message}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on video error ' + 'with no message', (tester) async { + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + final error = FakeMediaError(MediaError.MEDIA_ERR_NETWORK); + final errorCode = CameraErrorCode.fromMediaError(error); + + when(() => videoElement.error).thenReturn(error); + + errorStreamController.add(Event('error')); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${errorCode}, error message: No further diagnostic information can be determined or provided.', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on abort error', (tester) async { + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + abortStreamController.add(Event('abort')); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${CameraErrorCode.abort}, error message: The video element\'s source has not fully loaded.', + ), + ), + ); + + await streamQueue.cancel(); + }); }); testWidgets('onVideoRecordedEvent throws UnimplementedError', diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart index 54a4f594fe07..8af3a9c3cd81 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -2,11 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:html'; import 'dart:ui'; import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/types/types.dart'; import 'package:cross_file/cross_file.dart'; import 'package:mocktail/mocktail.dart'; @@ -22,6 +24,10 @@ class MockMediaStreamTrack extends Mock implements MediaStreamTrack {} class MockCamera extends Mock implements Camera {} +class MockCameraOptions extends Mock implements CameraOptions {} + +class MockVideoElement extends Mock implements VideoElement {} + class MockXFile extends Mock implements XFile {} /// A fake [MediaStream] that returns the provided [_videoTracks]. @@ -52,14 +58,57 @@ class FakeMediaDeviceInfo extends Fake implements MediaDeviceInfo { String? get kind => _kind; } -/// A fake [DomException] that returns the provided error [_name]. +/// A fake [MediaError] that returns the provided error [_code] and [_message]. +class FakeMediaError extends Fake implements MediaError { + FakeMediaError( + this._code, [ + String message = '', + ]) : _message = message; + + final int _code; + final String _message; + + @override + int get code => _code; + + @override + String? get message => _message; +} + +/// A fake [DomException] that returns the provided error [_name] and [_message]. class FakeDomException extends Fake implements DomException { - FakeDomException(this._name); + FakeDomException( + this._name, [ + String? message, + ]) : _message = message; final String _name; + final String? _message; @override String get name => _name; + + @override + String? get message => _message; +} + +/// A fake [ElementStream] that listens to the provided [_stream] on [listen]. +class FakeElementStream extends Fake + implements ElementStream { + FakeElementStream(this._stream); + + final Stream _stream; + + @override + StreamSubscription listen(void onData(T event)?, + {Function? onError, void onDone()?, bool? cancelOnError}) { + return _stream.listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } } /// Returns a video element with a blank stream of size [videoSize]. diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 06551705f056..9e469033dfc4 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -4,17 +4,20 @@ import 'dart:html' as html; import 'dart:ui'; -import 'shims/dart_ui.dart' as ui; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_web/src/types/camera_error_codes.dart'; -import 'package:camera_web/src/types/camera_options.dart'; +import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/types/types.dart'; + +import 'shims/dart_ui.dart' as ui; String _getViewType(int cameraId) => 'plugins.flutter.io/camera_$cameraId'; -/// A camera initialized from the media devices in the current [window]. -/// The obtained camera is constrained by the [options] used when -/// querying the media input in [_getMediaStream]. +/// A camera initialized from the media devices in the current window. +/// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices +/// +/// The obtained camera stream is constrained by [options] and fetched +/// with [CameraSettings.getMediaStreamForOptions]. /// /// The camera stream is displayed in the [videoElement] wrapped in the /// [divElement] to avoid overriding the custom styles applied to @@ -22,19 +25,19 @@ String _getViewType(int cameraId) => 'plugins.flutter.io/camera_$cameraId'; /// See: https://github.com/flutter/flutter/issues/79519 /// /// The camera can be played/stopped by calling [play]/[stop] -/// or may capture a picture by [takePicture]. +/// or may capture a picture by calling [takePicture]. /// /// The [textureId] is used to register a camera view with the id -/// returned by [_getViewType]. +/// defined by [_getViewType]. class Camera { /// Creates a new instance of [Camera] /// with the given [textureId] and optional /// [options] and [window]. Camera({ required this.textureId, + required CameraSettings cameraSettings, this.options = const CameraOptions(), - html.Window? window, - }) : window = window ?? html.window; + }) : _cameraSettings = cameraSettings; /// The texture id used to register the camera view. final int textureId; @@ -42,9 +45,6 @@ class Camera { /// The camera options used to initialize a camera, empty by default. final CameraOptions options; - /// The current browser window used to access device cameras. - final html.Window window; - /// The video element that displays the camera stream. /// Initialized in [initialize]. late html.VideoElement videoElement; @@ -54,16 +54,16 @@ class Camera { /// Initialized in [initialize]. late html.DivElement divElement; + /// The camera settings used to get the media stream for the camera. + final CameraSettings _cameraSettings; + /// Initializes the camera stream displayed in the [videoElement]. /// Registers the camera view with [textureId] under [_getViewType] type. Future initialize() async { - final isSupported = window.navigator.mediaDevices?.getUserMedia != null; - if (!isSupported) { - throw CameraException( - CameraErrorCodes.notSupported, - 'The camera is not supported on this device.', - ); - } + final stream = await _cameraSettings.getMediaStreamForOptions( + options, + cameraId: textureId, + ); videoElement = html.VideoElement(); _applyDefaultVideoStyles(videoElement); @@ -77,7 +77,6 @@ class Camera { (_) => divElement, ); - final stream = await _getMediaStream(); videoElement ..autoplay = false ..muted = !options.audio.enabled @@ -85,64 +84,15 @@ class Camera { ..setAttribute('playsinline', ''); } - Future _getMediaStream() async { - try { - final constraints = await options.toJson(); - return await window.navigator.mediaDevices!.getUserMedia(constraints); - } on html.DomException catch (e) { - switch (e.name) { - case 'NotFoundError': - case 'DevicesNotFoundError': - throw CameraException( - CameraErrorCodes.notFound, - 'No camera found for the given camera options.', - ); - case 'NotReadableError': - case 'TrackStartError': - throw CameraException( - CameraErrorCodes.notReadable, - 'The camera is not readable due to a hardware error ' - 'that prevented access to the device.', - ); - case 'OverconstrainedError': - case 'ConstraintNotSatisfiedError': - throw CameraException( - CameraErrorCodes.overconstrained, - 'The camera options are impossible to satisfy.', - ); - case 'NotAllowedError': - case 'PermissionDeniedError': - throw CameraException( - CameraErrorCodes.permissionDenied, - 'The camera cannot be used or the permission ' - 'to access the camera is not granted.', - ); - case 'TypeError': - throw CameraException( - CameraErrorCodes.type, - 'The camera options are incorrect or attempted' - 'to access the media input from an insecure context.', - ); - default: - throw CameraException( - CameraErrorCodes.unknown, - 'An unknown error occured when initializing the camera.', - ); - } - } catch (_) { - throw CameraException( - CameraErrorCodes.unknown, - 'An unknown error occured when initializing the camera.', - ); - } - } - /// Starts the camera stream. /// /// Initializes the camera source if the camera was previously stopped. Future play() async { if (videoElement.srcObject == null) { - final stream = await _getMediaStream(); + final stream = await _cameraSettings.getMediaStreamForOptions( + options, + cameraId: textureId, + ); videoElement.srcObject = stream; } await videoElement.play(); diff --git a/packages/camera/camera_web/lib/src/camera_settings.dart b/packages/camera/camera_web/lib/src/camera_settings.dart index 7b87840a90f8..1412248a2371 100644 --- a/packages/camera/camera_web/lib/src/camera_settings.dart +++ b/packages/camera/camera_web/lib/src/camera_settings.dart @@ -8,8 +8,10 @@ import 'dart:ui'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; -/// A utility to fetch and map camera settings. +/// A utility to fetch, map camera settings and +/// obtain the camera stream. class CameraSettings { // A facing mode constraint name. static const _facingModeKey = "facingMode"; @@ -18,6 +20,93 @@ class CameraSettings { @visibleForTesting html.Window? window = html.window; + /// Returns a media stream associated with the camera device + /// with [cameraId] and constrained by [options]. + Future getMediaStreamForOptions( + CameraOptions options, { + int cameraId = 0, + }) async { + final mediaDevices = window?.navigator.mediaDevices; + + // Throw a not supported exception if the current browser window + // does not support any media devices. + if (mediaDevices == null) { + throw PlatformException( + code: CameraErrorCode.notSupported.toString(), + message: 'The camera is not supported on this device.', + ); + } + + try { + final constraints = await options.toJson(); + return await mediaDevices.getUserMedia(constraints); + } on html.DomException catch (e) { + switch (e.name) { + case 'NotFoundError': + case 'DevicesNotFoundError': + throw CameraWebException( + cameraId, + CameraErrorCode.notFound, + 'No camera found for the given camera options.', + ); + case 'NotReadableError': + case 'TrackStartError': + throw CameraWebException( + cameraId, + CameraErrorCode.notReadable, + 'The camera is not readable due to a hardware error ' + 'that prevented access to the device.', + ); + case 'OverconstrainedError': + case 'ConstraintNotSatisfiedError': + throw CameraWebException( + cameraId, + CameraErrorCode.overconstrained, + 'The camera options are impossible to satisfy.', + ); + case 'NotAllowedError': + case 'PermissionDeniedError': + throw CameraWebException( + cameraId, + CameraErrorCode.permissionDenied, + 'The camera cannot be used or the permission ' + 'to access the camera is not granted.', + ); + case 'TypeError': + throw CameraWebException( + cameraId, + CameraErrorCode.type, + 'The camera options are incorrect or attempted' + 'to access the media input from an insecure context.', + ); + case 'AbortError': + throw CameraWebException( + cameraId, + CameraErrorCode.abort, + 'Some problem occurred that prevented the camera from being used.', + ); + case 'SecurityError': + throw CameraWebException( + cameraId, + CameraErrorCode.security, + 'The user media support is disabled in the current browser.', + ); + default: + throw CameraWebException( + cameraId, + CameraErrorCode.unknown, + 'An unknown error occured when fetching the camera stream.', + ); + } + } catch (_) { + throw CameraWebException( + cameraId, + CameraErrorCode.unknown, + 'An unknown error occured when fetching the camera stream.', + ); + } + } + /// Returns a facing mode of the [videoTrack] /// (null if the facing mode is not available). String? getFacingModeForVideoTrack(html.MediaStreamTrack videoTrack) { @@ -26,9 +115,9 @@ class CameraSettings { // Throw a not supported exception if the current browser window // does not support any media devices. if (mediaDevices == null) { - throw CameraException( - CameraErrorCodes.notSupported, - 'The camera is not supported on this device.', + throw PlatformException( + code: CameraErrorCode.notSupported.toString(), + message: 'The camera is not supported on this device.', ); } @@ -79,9 +168,10 @@ class CameraSettings { // Return null if getting capabilities is currently not supported. return null; default: - throw CameraException( - CameraErrorCodes.unknown, - 'An unknown error occured when getting the video track capabilities.', + throw PlatformException( + code: CameraErrorCode.unknown.toString(), + message: + 'An unknown error occured when getting the video track capabilities.', ); } } diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 4f130250970c..35241d0c9b8b 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -15,6 +15,11 @@ import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:stream_transform/stream_transform.dart'; +// The default error message, when the error is an empty string. +// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/message +const String _kDefaultErrorMessage = + 'No further diagnostic information can be determined or provided.'; + /// The web implementation of [CameraPlatform]. /// /// This class implements the `package:camera` functionality for the web. @@ -50,6 +55,12 @@ class CameraPlugin extends CameraPlatform { @visibleForTesting final cameraEventStreamController = StreamController.broadcast(); + final _cameraVideoErrorSubscriptions = + >{}; + + final _cameraVideoAbortSubscriptions = + >{}; + /// Returns a stream of camera events for the given [cameraId]. Stream _cameraEvents(int cameraId) => cameraEventStreamController.stream @@ -61,87 +72,103 @@ class CameraPlugin extends CameraPlatform { @override Future> availableCameras() async { - final mediaDevices = window?.navigator.mediaDevices; - final cameras = []; - - // Throw a not supported exception if the current browser window - // does not support any media devices. - if (mediaDevices == null) { - throw CameraException( - CameraErrorCodes.notSupported, - 'The camera is not supported on this device.', - ); - } - - // Request available media devices. - final devices = await mediaDevices.enumerateDevices(); - - // Filter video input devices. - final videoInputDevices = devices - .whereType() - .where((device) => device.kind == MediaDeviceKind.videoInput) - - /// The device id property is currently not supported on Internet Explorer: - /// https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId#browser_compatibility - .where((device) => device.deviceId != null); - - // Map video input devices to camera descriptions. - for (final videoInputDevice in videoInputDevices) { - // Get the video stream for the current video input device - // to later use for the available video tracks. - final videoStream = await _getVideoStreamForDevice( - mediaDevices, - videoInputDevice.deviceId!, - ); - - // Get all video tracks in the video stream - // to later extract the lens direction from the first track. - final videoTracks = videoStream.getVideoTracks(); - - if (videoTracks.isNotEmpty) { - // Get the facing mode from the first available video track. - final facingMode = _cameraSettings.getFacingModeForVideoTrack( - videoTracks.first, + try { + final mediaDevices = window?.navigator.mediaDevices; + final cameras = []; + + // Throw a not supported exception if the current browser window + // does not support any media devices. + if (mediaDevices == null) { + throw PlatformException( + code: CameraErrorCode.notSupported.toString(), + message: 'The camera is not supported on this device.', ); + } - // Get the lens direction based on the facing mode. - // Fallback to the external lens direction - // if the facing mode is not available. - final lensDirection = facingMode != null - ? _cameraSettings.mapFacingModeToLensDirection(facingMode) - : CameraLensDirection.external; - - // Create a camera description. - // - // The name is a camera label which might be empty - // if no permissions to media devices have been granted. - // - // MediaDeviceInfo.label: - // https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/label - // - // Sensor orientation is currently not supported. - final cameraLabel = videoInputDevice.label ?? ''; - final camera = CameraDescription( - name: cameraLabel, - lensDirection: lensDirection, - sensorOrientation: 0, - ); + // Request video and audio permissions. + await _cameraSettings.getMediaStreamForOptions( + CameraOptions( + audio: AudioConstraints(enabled: true), + ), + ); - final cameraMetadata = CameraMetadata( - deviceId: videoInputDevice.deviceId!, - facingMode: facingMode, + // Request available media devices. + final devices = await mediaDevices.enumerateDevices(); + + // Filter video input devices. + final videoInputDevices = devices + .whereType() + .where((device) => device.kind == MediaDeviceKind.videoInput) + + /// The device id property is currently not supported on Internet Explorer: + /// https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId#browser_compatibility + .where( + (device) => device.deviceId != null && device.deviceId!.isNotEmpty, + ); + + // Map video input devices to camera descriptions. + for (final videoInputDevice in videoInputDevices) { + // Get the video stream for the current video input device + // to later use for the available video tracks. + final videoStream = await _getVideoStreamForDevice( + videoInputDevice.deviceId!, ); - cameras.add(camera); - - camerasMetadata[camera] = cameraMetadata; - } else { - // Ignore as no video tracks exist in the current video input device. - continue; + // Get all video tracks in the video stream + // to later extract the lens direction from the first track. + final videoTracks = videoStream.getVideoTracks(); + + if (videoTracks.isNotEmpty) { + // Get the facing mode from the first available video track. + final facingMode = + _cameraSettings.getFacingModeForVideoTrack(videoTracks.first); + + // Get the lens direction based on the facing mode. + // Fallback to the external lens direction + // if the facing mode is not available. + final lensDirection = facingMode != null + ? _cameraSettings.mapFacingModeToLensDirection(facingMode) + : CameraLensDirection.external; + + // Create a camera description. + // + // The name is a camera label which might be empty + // if no permissions to media devices have been granted. + // + // MediaDeviceInfo.label: + // https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/label + // + // Sensor orientation is currently not supported. + final cameraLabel = videoInputDevice.label ?? ''; + final camera = CameraDescription( + name: cameraLabel, + lensDirection: lensDirection, + sensorOrientation: 0, + ); + + final cameraMetadata = CameraMetadata( + deviceId: videoInputDevice.deviceId!, + facingMode: facingMode, + ); + + cameras.add(camera); + + camerasMetadata[camera] = cameraMetadata; + } else { + // Ignore as no video tracks exist in the current video input device. + continue; + } } - } - return cameras; + return cameras; + } on html.DomException catch (e) { + throw CameraException(e.name, e.message); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw CameraException(e.code.toString(), e.description); + } } @override @@ -150,50 +177,56 @@ class CameraPlugin extends CameraPlatform { ResolutionPreset? resolutionPreset, { bool enableAudio = false, }) async { - if (!camerasMetadata.containsKey(cameraDescription)) { - throw CameraException( - CameraErrorCodes.missingMetadata, - 'Missing camera metadata. Make sure to call `availableCameras` before creating a camera.', - ); - } + try { + if (!camerasMetadata.containsKey(cameraDescription)) { + throw PlatformException( + code: CameraErrorCode.missingMetadata.toString(), + message: + 'Missing camera metadata. Make sure to call `availableCameras` before creating a camera.', + ); + } - final textureId = _textureCounter++; - - final cameraMetadata = camerasMetadata[cameraDescription]!; - - final cameraType = cameraMetadata.facingMode != null - ? _cameraSettings.mapFacingModeToCameraType(cameraMetadata.facingMode!) - : null; - - // Use the highest resolution possible - // if the resolution preset is not specified. - final videoSize = _cameraSettings - .mapResolutionPresetToSize(resolutionPreset ?? ResolutionPreset.max); - - // Create a camera with the given audio and video constraints. - // Sensor orientation is currently not supported. - final camera = Camera( - textureId: textureId, - window: window, - options: CameraOptions( - audio: AudioConstraints(enabled: enableAudio), - video: VideoConstraints( - facingMode: - cameraType != null ? FacingModeConstraint(cameraType) : null, - width: VideoSizeConstraint( - ideal: videoSize.width.toInt(), - ), - height: VideoSizeConstraint( - ideal: videoSize.height.toInt(), + final textureId = _textureCounter++; + + final cameraMetadata = camerasMetadata[cameraDescription]!; + + final cameraType = cameraMetadata.facingMode != null + ? _cameraSettings + .mapFacingModeToCameraType(cameraMetadata.facingMode!) + : null; + + // Use the highest resolution possible + // if the resolution preset is not specified. + final videoSize = _cameraSettings + .mapResolutionPresetToSize(resolutionPreset ?? ResolutionPreset.max); + + // Create a camera with the given audio and video constraints. + // Sensor orientation is currently not supported. + final camera = Camera( + textureId: textureId, + cameraSettings: _cameraSettings, + options: CameraOptions( + audio: AudioConstraints(enabled: enableAudio), + video: VideoConstraints( + facingMode: + cameraType != null ? FacingModeConstraint(cameraType) : null, + width: VideoSizeConstraint( + ideal: videoSize.width.toInt(), + ), + height: VideoSizeConstraint( + ideal: videoSize.height.toInt(), + ), + deviceId: cameraMetadata.deviceId, ), - deviceId: cameraMetadata.deviceId, ), - ), - ); + ); - cameras[textureId] = camera; + cameras[textureId] = camera; - return textureId; + return textureId; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } } @override @@ -202,26 +235,66 @@ class CameraPlugin extends CameraPlatform { // The image format group is currently not supported. ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, }) async { - final camera = getCamera(cameraId); - - await camera.initialize(); - await camera.play(); - - final cameraSize = await camera.getVideoSize(); - - cameraEventStreamController.add( - CameraInitializedEvent( - cameraId, - cameraSize.width, - cameraSize.height, - // TODO(camera_web): Add support for exposure mode and point (https://github.com/flutter/flutter/issues/86857). - ExposureMode.auto, - false, - // TODO(camera_web): Add support for focus mode and point (https://github.com/flutter/flutter/issues/86858). - FocusMode.auto, - false, - ), - ); + try { + final camera = getCamera(cameraId); + + await camera.initialize(); + + // Add camera's video error events to the camera events stream. + // The error event fires when the video element's source has failed to load, or can't be used. + _cameraVideoErrorSubscriptions[cameraId] = + camera.videoElement.onError.listen((html.Event _) { + // The Event itself (_) doesn't contain information about the actual error. + // We need to look at the HTMLMediaElement.error. + // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error + final error = camera.videoElement.error!; + final errorCode = CameraErrorCode.fromMediaError(error); + final errorMessage = + error.message != '' ? error.message : _kDefaultErrorMessage; + + cameraEventStreamController.add( + CameraErrorEvent( + cameraId, + 'Error code: ${errorCode}, error message: ${errorMessage}', + ), + ); + }); + + // Add camera's video abort events to the camera events stream. + // The abort event fires when the video element's source has not fully loaded. + _cameraVideoAbortSubscriptions[cameraId] = + camera.videoElement.onAbort.listen((html.Event _) { + cameraEventStreamController.add( + CameraErrorEvent( + cameraId, + 'Error code: ${CameraErrorCode.abort}, error message: The video element\'s source has not fully loaded.', + ), + ); + }); + + await camera.play(); + + final cameraSize = await camera.getVideoSize(); + + cameraEventStreamController.add( + CameraInitializedEvent( + cameraId, + cameraSize.width, + cameraSize.height, + // TODO(camera_web): Add support for exposure mode and point (https://github.com/flutter/flutter/issues/86857). + ExposureMode.auto, + false, + // TODO(camera_web): Add support for focus mode and point (https://github.com/flutter/flutter/issues/86858). + FocusMode.auto, + false, + ), + ); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } } @override @@ -241,7 +314,7 @@ class CameraPlugin extends CameraPlatform { @override Stream onCameraError(int cameraId) { - throw UnimplementedError('onCameraError() is not implemented.'); + return _cameraEvents(cameraId).whereType(); } @override @@ -271,7 +344,11 @@ class CameraPlugin extends CameraPlatform { @override Future takePicture(int cameraId) { - return getCamera(cameraId).takePicture(); + try { + return getCamera(cameraId).takePicture(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } } @override @@ -368,13 +445,21 @@ class CameraPlugin extends CameraPlatform { @override Future dispose(int cameraId) async { - getCamera(cameraId).dispose(); - cameras.remove(cameraId); + try { + getCamera(cameraId).dispose(); + await _cameraVideoErrorSubscriptions[cameraId]?.cancel(); + await _cameraVideoAbortSubscriptions[cameraId]?.cancel(); + + cameras.remove(cameraId); + _cameraVideoErrorSubscriptions.remove(cameraId); + _cameraVideoAbortSubscriptions.remove(cameraId); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } } /// Returns a media video stream for the device with the given [deviceId]. Future _getVideoStreamForDevice( - html.MediaDevices mediaDevices, String deviceId, ) { // Create camera options with the desired device id. @@ -382,7 +467,7 @@ class CameraPlugin extends CameraPlatform { video: VideoConstraints(deviceId: deviceId), ); - return mediaDevices.getUserMedia(cameraOptions.toJson()); + return _cameraSettings.getMediaStreamForOptions(cameraOptions); } /// Returns a camera for the given [cameraId]. @@ -393,12 +478,23 @@ class CameraPlugin extends CameraPlatform { final camera = cameras[cameraId]; if (camera == null) { - throw CameraException( - CameraErrorCodes.notFound, - 'No camera found for the given camera id $cameraId.', + throw PlatformException( + code: CameraErrorCode.notFound.toString(), + message: 'No camera found for the given camera id $cameraId.', ); } return camera; } + + /// Adds a [CameraErrorEvent], associated with the [exception], + /// to the stream of camera events. + void _addCameraErrorEvent(CameraWebException exception) { + cameraEventStreamController.add( + CameraErrorEvent( + exception.cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ); + } } diff --git a/packages/camera/camera_web/lib/src/types/camera_error_code.dart b/packages/camera/camera_web/lib/src/types/camera_error_code.dart new file mode 100644 index 000000000000..3dcace3ca2d6 --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/camera_error_code.dart @@ -0,0 +1,71 @@ +// 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 'dart:html' as html; + +/// Error codes that may occur during the camera initialization, +/// configuration or video streaming. +class CameraErrorCode { + const CameraErrorCode._(this._type); + + final String _type; + + @override + String toString() => _type; + + /// The camera is not supported. + static const CameraErrorCode notSupported = + CameraErrorCode._('cameraNotSupported'); + + /// The camera is not found. + static const CameraErrorCode notFound = CameraErrorCode._('cameraNotFound'); + + /// The camera is not readable. + static const CameraErrorCode notReadable = + CameraErrorCode._('cameraNotReadable'); + + /// The camera options are impossible to satisfy. + static const CameraErrorCode overconstrained = + CameraErrorCode._('cameraOverconstrained'); + + /// The camera cannot be used or the permission + /// to access the camera is not granted. + static const CameraErrorCode permissionDenied = + CameraErrorCode._('cameraPermission'); + + /// The camera options are incorrect or attempted + /// to access the media input from an insecure context. + static const CameraErrorCode type = CameraErrorCode._('cameraType'); + + /// Some problem occurred that prevented the camera from being used. + static const CameraErrorCode abort = CameraErrorCode._('cameraAbort'); + + /// The user media support is disabled in the current browser. + static const CameraErrorCode security = CameraErrorCode._('cameraSecurity'); + + /// The camera metadata is missing. + static const CameraErrorCode missingMetadata = + CameraErrorCode._('cameraMissingMetadata'); + + /// An unknown camera error. + static const CameraErrorCode unknown = CameraErrorCode._('cameraUnknown'); + + /// Returns a camera error code based on the media error. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code + static CameraErrorCode fromMediaError(html.MediaError error) { + switch (error.code) { + case html.MediaError.MEDIA_ERR_ABORTED: + return CameraErrorCode._('mediaErrorAborted'); + case html.MediaError.MEDIA_ERR_NETWORK: + return CameraErrorCode._('mediaErrorNetwork'); + case html.MediaError.MEDIA_ERR_DECODE: + return CameraErrorCode._('mediaErrorDecode'); + case html.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: + return CameraErrorCode._('mediaErrorSourceNotSupported'); + default: + return CameraErrorCode._('mediaErrorUnknown'); + } + } +} diff --git a/packages/camera/camera_web/lib/src/types/camera_error_codes.dart b/packages/camera/camera_web/lib/src/types/camera_error_codes.dart deleted file mode 100644 index afb02ae3aaa9..000000000000 --- a/packages/camera/camera_web/lib/src/types/camera_error_codes.dart +++ /dev/null @@ -1,32 +0,0 @@ -// 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. - -/// Error codes that may occur during the camera initialization or streaming. -abstract class CameraErrorCodes { - /// The camera is not supported. - static const notSupported = 'cameraNotSupported'; - - /// The camera is not found. - static const notFound = 'cameraNotFound'; - - /// The camera is not readable. - static const notReadable = 'cameraNotReadable'; - - /// The camera options are impossible to satisfy. - static const overconstrained = 'cameraOverconstrained'; - - /// The camera cannot be used or the permission - /// to access the camera is not granted. - static const permissionDenied = 'cameraPermission'; - - /// The camera options are incorrect or attempted - /// to access the media input from an insecure context. - static const type = 'cameraType'; - - /// The camera metadata is missing. - static const missingMetadata = 'missingMetadata'; - - /// An unknown camera error. - static const unknown = 'cameraUnknown'; -} diff --git a/packages/camera/camera_web/lib/src/types/camera_web_exception.dart b/packages/camera/camera_web/lib/src/types/camera_web_exception.dart new file mode 100644 index 000000000000..c21106cc462e --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/camera_web_exception.dart @@ -0,0 +1,29 @@ +// 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:camera_web/src/types/types.dart'; + +/// An exception thrown when the camera with id [cameraId] reports +/// an initialization, configuration or video streaming error, +/// or enters into an unexpected state. +/// +/// This error should be emitted on the `onCameraError` stream +/// of the camera platform. +class CameraWebException implements Exception { + /// Creates a new instance of [CameraWebException] + /// with the given error [cameraId], [code] and [description]. + CameraWebException(this.cameraId, this.code, this.description); + + /// The id of the camera this exception is associated to. + int cameraId; + + /// The error code of this exception. + CameraErrorCode code; + + /// The description of this exception. + String description; + + @override + String toString() => 'CameraWebException($cameraId, $code, $description)'; +} diff --git a/packages/camera/camera_web/lib/src/types/types.dart b/packages/camera/camera_web/lib/src/types/types.dart index 1a15503715cd..788ec79de205 100644 --- a/packages/camera/camera_web/lib/src/types/types.dart +++ b/packages/camera/camera_web/lib/src/types/types.dart @@ -2,7 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export 'camera_error_codes.dart'; +export 'camera_error_code.dart'; export 'camera_metadata.dart'; export 'camera_options.dart'; +export 'camera_web_exception.dart'; export 'media_device_kind.dart'; diff --git a/packages/camera/camera_web/test/helpers/helpers.dart b/packages/camera/camera_web/test/helpers/helpers.dart new file mode 100644 index 000000000000..7094f55bb62e --- /dev/null +++ b/packages/camera/camera_web/test/helpers/helpers.dart @@ -0,0 +1,5 @@ +// 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. + +export 'mocks.dart'; diff --git a/packages/camera/camera_web/test/helpers/mocks.dart b/packages/camera/camera_web/test/helpers/mocks.dart new file mode 100644 index 000000000000..0398ad33f126 --- /dev/null +++ b/packages/camera/camera_web/test/helpers/mocks.dart @@ -0,0 +1,17 @@ +// 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 'dart:html'; + +import 'package:flutter_test/flutter_test.dart'; + +/// A fake [MediaError] that returns the provided error [_code]. +class FakeMediaError extends Fake implements MediaError { + FakeMediaError(this._code); + + final int _code; + + @override + int get code => _code; +} diff --git a/packages/camera/camera_web/test/types/camera_error_code_test.dart b/packages/camera/camera_web/test/types/camera_error_code_test.dart new file mode 100644 index 000000000000..ca896e8696d7 --- /dev/null +++ b/packages/camera/camera_web/test/types/camera_error_code_test.dart @@ -0,0 +1,133 @@ +// 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 'dart:html'; + +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../helpers/helpers.dart'; + +void main() { + group('CameraErrorCode', () { + group('toString returns a correct type for', () { + test('notSupported', () { + expect( + CameraErrorCode.notSupported.toString(), + equals('cameraNotSupported'), + ); + }); + + test('notFound', () { + expect( + CameraErrorCode.notFound.toString(), + equals('cameraNotFound'), + ); + }); + + test('notReadable', () { + expect( + CameraErrorCode.notReadable.toString(), + equals('cameraNotReadable'), + ); + }); + + test('overconstrained', () { + expect( + CameraErrorCode.overconstrained.toString(), + equals('cameraOverconstrained'), + ); + }); + + test('permissionDenied', () { + expect( + CameraErrorCode.permissionDenied.toString(), + equals('cameraPermission'), + ); + }); + + test('type', () { + expect( + CameraErrorCode.type.toString(), + equals('cameraType'), + ); + }); + + test('abort', () { + expect( + CameraErrorCode.abort.toString(), + equals('cameraAbort'), + ); + }); + + test('security', () { + expect( + CameraErrorCode.security.toString(), + equals('cameraSecurity'), + ); + }); + + test('missingMetadata', () { + expect( + CameraErrorCode.missingMetadata.toString(), + equals('cameraMissingMetadata'), + ); + }); + + test('unknown', () { + expect( + CameraErrorCode.unknown.toString(), + equals('cameraUnknown'), + ); + }); + + group('fromMediaError', () { + test('with aborted error code', () { + expect( + CameraErrorCode.fromMediaError( + FakeMediaError(MediaError.MEDIA_ERR_ABORTED), + ).toString(), + equals('mediaErrorAborted'), + ); + }); + + test('with network error code', () { + expect( + CameraErrorCode.fromMediaError( + FakeMediaError(MediaError.MEDIA_ERR_NETWORK), + ).toString(), + equals('mediaErrorNetwork'), + ); + }); + + test('with decode error code', () { + expect( + CameraErrorCode.fromMediaError( + FakeMediaError(MediaError.MEDIA_ERR_DECODE), + ).toString(), + equals('mediaErrorDecode'), + ); + }); + + test('with source not supported error code', () { + expect( + CameraErrorCode.fromMediaError( + FakeMediaError(MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED), + ).toString(), + equals('mediaErrorSourceNotSupported'), + ); + }); + + test('with unknown error code', () { + expect( + CameraErrorCode.fromMediaError( + FakeMediaError(5), + ).toString(), + equals('mediaErrorUnknown'), + ); + }); + }); + }); + }); +} diff --git a/packages/camera/camera_web/test/types/camera_web_exception_test.dart b/packages/camera/camera_web/test/types/camera_web_exception_test.dart new file mode 100644 index 000000000000..d58512b460e2 --- /dev/null +++ b/packages/camera/camera_web/test/types/camera_web_exception_test.dart @@ -0,0 +1,35 @@ +// 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:camera_web/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('CameraWebException', () { + test('sets all properties', () { + final cameraId = 1; + final code = CameraErrorCode.notFound; + final description = 'The camera is not found.'; + + final exception = CameraWebException(cameraId, code, description); + + expect(exception.cameraId, equals(cameraId)); + expect(exception.code, equals(code)); + expect(exception.description, equals(description)); + }); + + test('toString includes all properties', () { + final cameraId = 2; + final code = CameraErrorCode.notReadable; + final description = 'The camera is not readable.'; + + final exception = CameraWebException(cameraId, code, description); + + expect( + exception.toString(), + equals('CameraWebException($cameraId, $code, $description)'), + ); + }); + }); +} From a3accd7c2fc72150c225a919d336819066759620 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 2 Aug 2021 09:52:06 +0200 Subject: [PATCH 046/123] Replace reference to shared_preferences with url_launcher (#4211) --- packages/url_launcher/url_launcher_linux/CHANGELOG.md | 4 ++++ packages/url_launcher/url_launcher_linux/README.md | 2 +- packages/url_launcher/url_launcher_linux/pubspec.yaml | 2 +- packages/url_launcher/url_launcher_macos/CHANGELOG.md | 4 ++++ packages/url_launcher/url_launcher_macos/README.md | 2 +- packages/url_launcher/url_launcher_macos/pubspec.yaml | 2 +- packages/url_launcher/url_launcher_web/CHANGELOG.md | 4 ++++ packages/url_launcher/url_launcher_web/README.md | 2 +- packages/url_launcher/url_launcher_web/pubspec.yaml | 2 +- packages/url_launcher/url_launcher_windows/CHANGELOG.md | 4 ++++ packages/url_launcher/url_launcher_windows/README.md | 2 +- packages/url_launcher/url_launcher_windows/pubspec.yaml | 2 +- 12 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/url_launcher/url_launcher_linux/CHANGELOG.md b/packages/url_launcher/url_launcher_linux/CHANGELOG.md index b872a55ef161..147d0f312c7e 100644 --- a/packages/url_launcher/url_launcher_linux/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.2 + +* Replaced reference to `shared_preferences` plugin with the `url_launcher` in the README. + ## 2.0.1 * Updated installation instructions in README. diff --git a/packages/url_launcher/url_launcher_linux/README.md b/packages/url_launcher/url_launcher_linux/README.md index 23c0019b6948..1d0667860030 100644 --- a/packages/url_launcher/url_launcher_linux/README.md +++ b/packages/url_launcher/url_launcher_linux/README.md @@ -4,7 +4,7 @@ The Linux implementation of [`url_launcher`][1]. ## Usage -This package is [endorsed][2], which means you can simply use `shared_preferences` +This package is [endorsed][2], which means you can simply use `url_launcher` normally. This package will be automatically included in your app when you do. [1]: https://pub.dev/packages/url_launcher diff --git a/packages/url_launcher/url_launcher_linux/pubspec.yaml b/packages/url_launcher/url_launcher_linux/pubspec.yaml index e08011e496d5..960216851e5d 100644 --- a/packages/url_launcher/url_launcher_linux/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_linux description: Linux implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 2.0.1 +version: 2.0.2 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/url_launcher/url_launcher_macos/CHANGELOG.md b/packages/url_launcher/url_launcher_macos/CHANGELOG.md index 2f672940f2ac..96d2fd49c7e7 100644 --- a/packages/url_launcher/url_launcher_macos/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_macos/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.2 + +* Replaced reference to `shared_preferences` plugin with the `url_launcher` in the README. + ## 2.0.1 * Add native unit tests. diff --git a/packages/url_launcher/url_launcher_macos/README.md b/packages/url_launcher/url_launcher_macos/README.md index b594cde1d041..0869f0ce9940 100644 --- a/packages/url_launcher/url_launcher_macos/README.md +++ b/packages/url_launcher/url_launcher_macos/README.md @@ -4,7 +4,7 @@ The macos implementation of [`url_launcher`][1]. ## Usage -This package is [endorsed][2], which means you can simply use `shared_preferences` +This package is [endorsed][2], which means you can simply use `url_launcher` normally. This package will be automatically included in your app when you do. [1]: https://pub.dev/packages/url_launcher diff --git a/packages/url_launcher/url_launcher_macos/pubspec.yaml b/packages/url_launcher/url_launcher_macos/pubspec.yaml index 2483e35e56de..534830000626 100644 --- a/packages/url_launcher/url_launcher_macos/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_macos description: macOS implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 2.0.1 +version: 2.0.2 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index b1fff136793d..64830f5e4481 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.3 + +- Replaced reference to `shared_preferences` plugin with the `url_launcher` in the README. + ## 2.0.2 - Updated installation instructions in README. diff --git a/packages/url_launcher/url_launcher_web/README.md b/packages/url_launcher/url_launcher_web/README.md index b03d15478ee3..8043c9fa07ff 100644 --- a/packages/url_launcher/url_launcher_web/README.md +++ b/packages/url_launcher/url_launcher_web/README.md @@ -4,7 +4,7 @@ The web implementation of [`url_launcher`][1]. ## Usage -This package is [endorsed][2], which means you can simply use `shared_preferences` +This package is [endorsed][2], which means you can simply use `url_launcher` normally. This package will be automatically included in your app when you do. [1]: https://pub.dev/packages/url_launcher diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml index dbb658d5fb1f..cba098daceb7 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_web description: Web platform implementation of url_launcher repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 2.0.2 +version: 2.0.3 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md index fca798364f6f..d26fe19c359e 100644 --- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.2 + +* Replaced reference to `shared_preferences` plugin with the `url_launcher` in the README. + ## 2.0.1 * Updated installation instructions in README. diff --git a/packages/url_launcher/url_launcher_windows/README.md b/packages/url_launcher/url_launcher_windows/README.md index 307f518c4cac..cd7b6d47eeb2 100644 --- a/packages/url_launcher/url_launcher_windows/README.md +++ b/packages/url_launcher/url_launcher_windows/README.md @@ -4,7 +4,7 @@ The Windows implementation of [`url_launcher`][1]. ## Usage -This package is [endorsed][2], which means you can simply use `shared_preferences` +This package is [endorsed][2], which means you can simply use `url_launcher` normally. This package will be automatically included in your app when you do. [1]: https://pub.dev/packages/url_launcher diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml index 4d330dd826d5..6435eda4564a 100644 --- a/packages/url_launcher/url_launcher_windows/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_windows description: Windows implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 2.0.1 +version: 2.0.2 environment: sdk: ">=2.12.0 <3.0.0" From 9bca9e7c8435a8a9dd321975f795e205aaa11502 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 3 Aug 2021 10:22:46 -0700 Subject: [PATCH 047/123] [google_maps_flutter] Temporarily disable googleMapsPluginIsAdded (#4214) Currently has an out-of-band failure on master. Ignoring to re-open the tree. --- .../io/flutter/plugins/googlemapsexample/GoogleMapsTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java index 40552ddf7be1..43ddeaae1579 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java @@ -8,9 +8,11 @@ import androidx.test.core.app.ActivityScenario; import io.flutter.plugins.googlemaps.GoogleMapsPlugin; +import org.junit.Ignore; import org.junit.Test; public class GoogleMapsTest { + @Ignore("Currently failing: https://github.com/flutter/flutter/issues/87566") @Test public void googleMapsPluginIsAdded() { final ActivityScenario scenario = From c59b32ce495ca3890e5250a884c14e8606b83599 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Tue, 3 Aug 2021 12:00:05 -0700 Subject: [PATCH 048/123] [google_sign_in] Mark iOS arm64 simulators as unsupported (#4208) --- packages/google_sign_in/google_sign_in/CHANGELOG.md | 4 ++++ .../google_sign_in/google_sign_in/ios/google_sign_in.podspec | 4 +++- packages/google_sign_in/google_sign_in/pubspec.yaml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 2602e98be2a0..e4207de117fa 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.0.7 + +* Mark iOS arm64 simulators as unsupported. + ## 5.0.6 * Remove references to the Android V1 embedding. diff --git a/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec b/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec index 6b0741c65122..a0b73276fafa 100644 --- a/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec +++ b/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec @@ -20,5 +20,7 @@ Enables Google Sign-In in Flutter apps. s.static_framework = true s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + + # GoogleSignIn ~> 5.0 does not support arm64 simulators. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } end diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index bbcdbc91d71e..7e3f221716a8 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. repository: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 5.0.6 +version: 5.0.7 environment: sdk: ">=2.12.0 <3.0.0" From 1fc3d927cd44835fb2db2263e850fab8f2bb7661 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Tue, 3 Aug 2021 12:05:09 -0700 Subject: [PATCH 049/123] [google_maps_flutter] Mark iOS arm64 simulators as unsupported (#4209) --- packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md | 4 ++++ .../example/ios/Flutter/AppFrameworkInfo.plist | 2 +- .../example/ios/Runner.xcodeproj/project.pbxproj | 4 ++-- .../google_maps_flutter/ios/google_maps_flutter.podspec | 3 ++- packages/google_maps_flutter/google_maps_flutter/pubspec.yaml | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 6ffec4e65cc4..3080d4a2d733 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.8 + +* Mark iOS arm64 simulators as unsupported. + ## 2.0.7 * Add iOS unit and UI integration test targets. diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/packages/google_maps_flutter/google_maps_flutter/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj index cfaff19656f2..fbb006aeded0 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -535,7 +535,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -585,7 +585,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec b/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec index 9a1f04d59759..292dda006fa4 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec +++ b/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec @@ -20,5 +20,6 @@ Downloaded by pub (not CocoaPods). s.dependency 'GoogleMaps' s.static_framework = true s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + # GoogleMaps does not support arm64 simulators. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } end diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index c784e9a37a94..f1dc21ae2600 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.0.7 +version: 2.0.8 environment: sdk: '>=2.12.0 <3.0.0' From 643c928c2f2ff52e05dd5c5e695ddf155e9ed170 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 3 Aug 2021 12:10:07 -0700 Subject: [PATCH 050/123] [image_picker] Check for failure in iOS metadata updates (#4215) --- .../image_picker/image_picker/CHANGELOG.md | 7 ++--- .../ios/RunnerTests/MetaDataUtilTests.m | 10 ++++++- .../ios/Classes/FLTImagePickerMetaDataUtil.h | 6 ++++- .../ios/Classes/FLTImagePickerMetaDataUtil.m | 27 +++++++++++++------ .../Classes/FLTImagePickerPhotoAssetUtil.m | 6 ++++- .../image_picker/image_picker/pubspec.yaml | 2 +- 6 files changed, 43 insertions(+), 15 deletions(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index e7048c371a95..bd0a7a06b4fc 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 0.8.3 * Move `ImagePickerFromLimitedGalleryUITests` to `RunnerUITests` target. +* Improved handling of bad image data when applying metadata changes on iOS. ## 0.8.2 @@ -53,8 +54,8 @@ see: [#84634](https://github.com/flutter/flutter/issues/84634). ## 0.8.0 * BREAKING CHANGE: Changed storage location for captured images and videos to internal cache on Android, -to comply with new Google Play storage requirements. This means developers are responsible for moving -the image or video to a different location in case more permanent storage is required. Other applications +to comply with new Google Play storage requirements. This means developers are responsible for moving +the image or video to a different location in case more permanent storage is required. Other applications will no longer be able to access images or videos captured unless they are moved to a publicly accessible location. * Updated Mockito to fix Android tests. diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/MetaDataUtilTests.m b/packages/image_picker/image_picker/example/ios/RunnerTests/MetaDataUtilTests.m index e1dbfad77b5d..54f9469f2053 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerTests/MetaDataUtilTests.m +++ b/packages/image_picker/image_picker/example/ios/RunnerTests/MetaDataUtilTests.m @@ -60,7 +60,7 @@ - (void)testWriteMetaData { NSString *tmpFile = [NSString stringWithFormat:@"image_picker_test.jpg"]; NSString *tmpDirectory = NSTemporaryDirectory(); NSString *tmpPath = [tmpDirectory stringByAppendingPathComponent:tmpFile]; - NSData *newData = [FLTImagePickerMetaDataUtil updateMetaData:metaData toImage:dataJPG]; + NSData *newData = [FLTImagePickerMetaDataUtil imageFromImage:dataJPG withMetaData:metaData]; if ([[NSFileManager defaultManager] createFileAtPath:tmpPath contents:newData attributes:nil]) { NSData *savedTmpImageData = [NSData dataWithContentsOfFile:tmpPath]; NSDictionary *tmpMetaData = @@ -71,6 +71,14 @@ - (void)testWriteMetaData { } } +- (void)testUpdateMetaDataBadData { + NSData *imageData = [NSData data]; + + NSDictionary *metaData = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:imageData]; + NSData *newData = [FLTImagePickerMetaDataUtil imageFromImage:imageData withMetaData:metaData]; + XCTAssertNil(newData); +} + - (void)testConvertImageToData { UIImage *imageJPG = [UIImage imageWithData:ImagePickerTestImages.JPGTestData]; NSData *convertedDataJPG = [FLTImagePickerMetaDataUtil convertImage:imageJPG diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h index d5a20ffc6d2e..72a36a56d57d 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h @@ -27,7 +27,11 @@ extern const FLTImagePickerMIMEType kFLTImagePickerMIMETypeDefault; + (NSDictionary *)getMetaDataFromImageData:(NSData *)imageData; -+ (NSData *)updateMetaData:(NSDictionary *)metaData toImage:(NSData *)imageData; +// Creates and returns data for a new image based on imageData, but with the +// given metadata. +// +// If creating a new image fails, returns nil. ++ (nullable NSData *)imageFromImage:(NSData *)imageData withMetaData:(NSDictionary *)metadata; // Converting UIImage to a NSData with the type proveide. // diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m index 1419584a4675..45bcaa7191f7 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m @@ -49,16 +49,27 @@ + (NSDictionary *)getMetaDataFromImageData:(NSData *)imageData { return metadata; } -+ (NSData *)updateMetaData:(NSDictionary *)metaData toImage:(NSData *)imageData { - NSMutableData *mutableData = [NSMutableData data]; - CGImageSourceRef cgImage = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL); - CGImageDestinationRef destination = CGImageDestinationCreateWithData( - (__bridge CFMutableDataRef)mutableData, CGImageSourceGetType(cgImage), 1, nil); - CGImageDestinationAddImageFromSource(destination, cgImage, 0, (__bridge CFDictionaryRef)metaData); ++ (NSData *)imageFromImage:(NSData *)imageData withMetaData:(NSDictionary *)metadata { + NSMutableData *targetData = [NSMutableData data]; + CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL); + if (source == NULL) { + return nil; + } + CGImageDestinationRef destination = NULL; + CFStringRef sourceType = CGImageSourceGetType(source); + if (sourceType != NULL) { + destination = + CGImageDestinationCreateWithData((__bridge CFMutableDataRef)targetData, sourceType, 1, nil); + } + if (destination == NULL) { + CFRelease(source); + return nil; + } + CGImageDestinationAddImageFromSource(destination, source, 0, (__bridge CFDictionaryRef)metadata); CGImageDestinationFinalize(destination); - CFRelease(cgImage); + CFRelease(source); CFRelease(destination); - return mutableData; + return targetData; } + (NSData *)convertImage:(UIImage *)image diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m index ab881790d5ab..4c705fe54350 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m @@ -86,7 +86,11 @@ + (NSString *)saveImageWithMetaData:(NSDictionary *)metaData usingType:type quality:imageQuality]; if (metaData) { - data = [FLTImagePickerMetaDataUtil updateMetaData:metaData toImage:data]; + NSData *updatedData = [FLTImagePickerMetaDataUtil imageFromImage:data withMetaData:metaData]; + // If updating the metadata fails, just save the original. + if (updatedData) { + data = updatedData; + } } return [self createFile:data suffix:suffix]; diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index e5ecfeb22232..f56250f53715 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.2 +version: 0.8.3 environment: sdk: ">=2.12.0 <3.0.0" From 498df33f7bd613e7f0dba16ebeff42ff98ee61c5 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 3 Aug 2021 16:24:46 -0700 Subject: [PATCH 051/123] [flutter_plugin_tools] Track and log exclusions (#4205) Makes commands that use the package-looping base command track and report exclusions. This will make it much easier to debug/audit situations where tests aren't running when expected (e.g., when enabling a new type of test for a package that previously had to be explicitly excluded from that test to avoid failing for having no tests, but forgetting to remove the package from the exclusion list). Also fixes a latent issue with using different exclusion lists on different commands in a single CI task when using sharding could cause unexpected failures due to different sets of plugins being included for each step (e.g., build+drive with an exclude list on drive could potentially try to drive a plugin that hadn't been built in that shard) by sharding before filtering out excluded packages. Adds testing for sharding in general, as there was previously none. --- script/tool/CHANGELOG.md | 2 + script/tool/lib/src/analyze_command.dart | 6 +- .../src/common/package_looping_command.dart | 90 +++++-- .../tool/lib/src/common/plugin_command.dart | 147 +++++++---- .../src/create_all_plugins_app_command.dart | 9 +- script/tool/lib/src/list_command.dart | 15 +- .../common/package_looping_command_test.dart | 76 ++++++ .../tool/test/common/plugin_command_test.dart | 239 +++++++++++++++--- 8 files changed, 460 insertions(+), 124 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 7d1eac01b760..7f326ff3c8f7 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -21,6 +21,8 @@ `--no-integration`. - **Breaking change**: Replaced `java-test` with Android unit test support for the new `native-test` command. +- Commands that print a run summary at the end now track and log exclusions + similarly to skips for easier auditing. ## 0.4.1 diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 4fd15f027f50..2b728e2b9073 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_command.dart'; import 'package:platform/platform.dart'; import 'package:yaml/yaml.dart'; @@ -84,7 +85,10 @@ class AnalyzeCommand extends PackageLoopingCommand { /// Ensures that the dependent packages have been fetched for all packages /// (including their sub-packages) that will be analyzed. Future _runPackagesGetOnTargetPackages() async { - final List packageDirectories = await getPackages().toList(); + final List packageDirectories = + await getTargetPackagesAndSubpackages() + .map((PackageEnumerationEntry package) => package.directory) + .toList(); final Set packagePaths = packageDirectories.map((Directory dir) => dir.path).toSet(); packageDirectories.removeWhere((Directory directory) { diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 0bcde6d296d3..0e0976ecc6a7 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -22,6 +22,10 @@ enum RunState { /// The command was skipped for the package. skipped, + /// The command was skipped for the package because it was explicitly excluded + /// in the command arguments. + excluded, + /// The command failed for the package. failed, } @@ -35,6 +39,9 @@ class PackageResult { PackageResult.skip(String reason) : this._(RunState.skipped, [reason]); + /// A run that was excluded by the command invocation. + PackageResult.exclude() : this._(RunState.excluded); + /// A run that failed. /// /// If [errors] are provided, they will be listed in the summary, otherwise @@ -70,13 +77,14 @@ abstract class PackageLoopingCommand extends PluginCommand { processRunner: processRunner, platform: platform, gitDir: gitDir); /// Packages that had at least one [logWarning] call. - final Set _packagesWithWarnings = {}; + final Set _packagesWithWarnings = + {}; /// Number of warnings that happened outside of a [runForPackage] call. int _otherWarningCount = 0; /// The package currently being run by [runForPackage]. - Directory? _currentPackage; + PackageEnumerationEntry? _currentPackage; /// Called during [run] before any calls to [runForPackage]. This provides an /// opportunity to fail early if the command can't be run (e.g., because the @@ -215,15 +223,24 @@ abstract class PackageLoopingCommand extends PluginCommand { await initializeRun(); - final List packages = includeSubpackages - ? await getPackages().toList() - : await getPlugins().toList(); + final List packages = includeSubpackages + ? await getTargetPackagesAndSubpackages(filterExcluded: false).toList() + : await getTargetPackages(filterExcluded: false).toList(); - final Map results = {}; - for (final Directory package in packages) { + final Map results = + {}; + for (final PackageEnumerationEntry package in packages) { _currentPackage = package; _printPackageHeading(package); - final PackageResult result = await runForPackage(package); + + // Command implementations should never see excluded packages; they are + // included at this level only for logging. + if (package.excluded) { + results[package] = PackageResult.exclude(); + continue; + } + + final PackageResult result = await runForPackage(package.directory); if (result.state == RunState.skipped) { final String message = '${indentation}SKIPPING: ${result.details.first}'; @@ -266,8 +283,11 @@ abstract class PackageLoopingCommand extends PluginCommand { /// Something is always printed to make it easier to distinguish between /// a command running for a package and producing no output, and a command /// not having been run for a package. - void _printPackageHeading(Directory package) { - String heading = 'Running for ${getPackageDescription(package)}'; + void _printPackageHeading(PackageEnumerationEntry package) { + final String packageDisplayName = getPackageDescription(package.directory); + String heading = package.excluded + ? 'Not running for $packageDisplayName; excluded' + : 'Running for $packageDisplayName'; if (hasLongOutput) { heading = ''' @@ -275,24 +295,35 @@ abstract class PackageLoopingCommand extends PluginCommand { || $heading ============================================================ '''; - } else { + } else if (!package.excluded) { heading = '$heading...'; } - captureOutput ? print(heading) : print(Colorize(heading)..cyan()); + if (captureOutput) { + print(heading); + } else { + final Colorize colorizeHeading = Colorize(heading); + print(package.excluded + ? colorizeHeading.darkGray() + : colorizeHeading.cyan()); + } } /// Prints a summary of packges run, packages skipped, and warnings. - void _printRunSummary( - List packages, Map results) { - final Set skippedPackages = results.entries - .where((MapEntry entry) => + void _printRunSummary(List packages, + Map results) { + final Set skippedPackages = results.entries + .where((MapEntry entry) => entry.value.state == RunState.skipped) - .map((MapEntry entry) => entry.key) + .map((MapEntry entry) => + entry.key) .toSet(); - final int skipCount = skippedPackages.length; + final int skipCount = skippedPackages.length + + packages + .where((PackageEnumerationEntry package) => package.excluded) + .length; // Split the warnings into those from packages that ran, and those that // were skipped. - final Set _skippedPackagesWithWarnings = + final Set _skippedPackagesWithWarnings = _packagesWithWarnings.intersection(skippedPackages); final int skippedWarningCount = _skippedPackagesWithWarnings.length; final int runWarningCount = @@ -318,14 +349,17 @@ abstract class PackageLoopingCommand extends PluginCommand { /// Prints a one-line-per-package overview of the run results for each /// package. - void _printPerPackageRunOverview(List packages, - {required Set skipped}) { + void _printPerPackageRunOverview(List packages, + {required Set skipped}) { print('Run overview:'); - for (final Directory package in packages) { + for (final PackageEnumerationEntry package in packages) { final bool hadWarning = _packagesWithWarnings.contains(package); Styles style; String summary; - if (skipped.contains(package)) { + if (package.excluded) { + summary = 'excluded'; + style = Styles.DARK_GRAY; + } else if (skipped.contains(package)) { summary = 'skipped'; style = hadWarning ? Styles.LIGHT_YELLOW : Styles.DARK_GRAY; } else { @@ -339,17 +373,17 @@ abstract class PackageLoopingCommand extends PluginCommand { if (!captureOutput) { summary = (Colorize(summary)..apply(style)).toString(); } - print(' ${getPackageDescription(package)} - $summary'); + print(' ${getPackageDescription(package.directory)} - $summary'); } print(''); } /// Prints a summary of all of the failures from [results]. - void _printFailureSummary( - List packages, Map results) { + void _printFailureSummary(List packages, + Map results) { const String indentation = ' '; _printError(failureListHeader); - for (final Directory package in packages) { + for (final PackageEnumerationEntry package in packages) { final PackageResult result = results[package]!; if (result.state == RunState.failed) { final String errorIndentation = indentation * 2; @@ -359,7 +393,7 @@ abstract class PackageLoopingCommand extends PluginCommand { ':\n$errorIndentation${result.details.join('\n$errorIndentation')}'; } _printError( - '$indentation${getPackageDescription(package)}$errorDetails'); + '$indentation${getPackageDescription(package.directory)}$errorDetails'); } } _printError(failureListFooter); diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 7781eee0d961..db0a821fd2d7 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -15,6 +15,19 @@ import 'core.dart'; import 'git_version_finder.dart'; import 'process_runner.dart'; +/// An entry in package enumeration for APIs that need to include extra +/// data about the entry. +class PackageEnumerationEntry { + /// Creates a new entry for the given package directory. + PackageEnumerationEntry(this.directory, {required this.excluded}); + + /// The package's location. + final Directory directory; + + /// Whether or not this package was excluded by the command invocation. + final bool excluded; +} + /// Interface definition for all commands in this tool. // TODO(stuartmorgan): Move most of this logic to PackageLoopingCommand. abstract class PluginCommand extends Command { @@ -97,6 +110,9 @@ abstract class PluginCommand extends Command { int? _shardIndex; int? _shardCount; + // Cached set of explicitly excluded packages. + Set? _excludedPackages; + /// A context that matches the default for [platform]. p.Context get path => platform.isWindows ? p.windows : p.posix; @@ -174,60 +190,82 @@ abstract class PluginCommand extends Command { _shardCount = shardCount; } - /// Returns the root Dart package folders of the plugins involved in this - /// command execution. - // TODO(stuartmorgan): Rename/restructure this, _getAllPlugins, and - // getPackages, as the current naming is very confusing. - Stream getPlugins() async* { + /// Returns the set of plugins to exclude based on the `--exclude` argument. + Set _getExcludedPackageName() { + final Set excludedPackages = _excludedPackages ?? + getStringListArg(_excludeArg).expand((String item) { + if (item.endsWith('.yaml')) { + final File file = packagesDir.fileSystem.file(item); + return (loadYaml(file.readAsStringSync()) as YamlList) + .toList() + .cast(); + } + return [item]; + }).toSet(); + // Cache for future calls. + _excludedPackages = excludedPackages; + return excludedPackages; + } + + /// Returns the root diretories of the packages involved in this command + /// execution. + /// + /// Depending on the command arguments, this may be a user-specified set of + /// packages, the set of packages that should be run for a given diff, or all + /// packages. + /// + /// By default, packages excluded via --exclude will not be in the stream, but + /// they can be included by passing false for [filterExcluded]. + Stream getTargetPackages( + {bool filterExcluded = true}) async* { // To avoid assuming consistency of `Directory.list` across command // invocations, we collect and sort the plugin folders before sharding. // This is considered an implementation detail which is why the API still // uses streams. - final List allPlugins = await _getAllPlugins().toList(); - allPlugins.sort((Directory d1, Directory d2) => d1.path.compareTo(d2.path)); - // Sharding 10 elements into 3 shards should yield shard sizes 4, 4, 2. - // Sharding 9 elements into 3 shards should yield shard sizes 3, 3, 3. - // Sharding 2 elements into 3 shards should yield shard sizes 1, 1, 0. + final List allPlugins = + await _getAllPackages().toList(); + allPlugins.sort((PackageEnumerationEntry p1, PackageEnumerationEntry p2) => + p1.directory.path.compareTo(p2.directory.path)); final int shardSize = allPlugins.length ~/ shardCount + (allPlugins.length % shardCount == 0 ? 0 : 1); final int start = min(shardIndex * shardSize, allPlugins.length); final int end = min(start + shardSize, allPlugins.length); - for (final Directory plugin in allPlugins.sublist(start, end)) { - yield plugin; + for (final PackageEnumerationEntry plugin + in allPlugins.sublist(start, end)) { + if (!(filterExcluded && plugin.excluded)) { + yield plugin; + } } } - /// Returns the root Dart package folders of the plugins involved in this - /// command execution, assuming there is only one shard. + /// Returns the root Dart package folders of the packages involved in this + /// command execution, assuming there is only one shard. Depending on the + /// command arguments, this may be a user-specified set of packages, the + /// set of packages that should be run for a given diff, or all packages. + /// + /// This will return packages that have been excluded by the --exclude + /// parameter, annotated in the entry as excluded. /// - /// Plugin packages can exist in the following places relative to the packages + /// Packages can exist in the following places relative to the packages /// directory: /// /// 1. As a Dart package in a directory which is a direct child of the - /// packages directory. This is a plugin where all of the implementations - /// exist in a single Dart package. + /// packages directory. This is a non-plugin package, or a non-federated + /// plugin. /// 2. Several plugin packages may live in a directory which is a direct /// child of the packages directory. This directory groups several Dart - /// packages which implement a single plugin. This directory contains a - /// "client library" package, which declares the API for the plugin, as - /// well as one or more platform-specific implementations. + /// packages which implement a single plugin. This directory contains an + /// "app-facing" package which declares the API for the plugin, a + /// platform interface package which declares the API for implementations, + /// and one or more platform-specific implementation packages. /// 3./4. Either of the above, but in a third_party/packages/ directory that /// is a sibling of the packages directory. This is used for a small number /// of packages in the flutter/packages repository. - Stream _getAllPlugins() async* { + Stream _getAllPackages() async* { Set plugins = Set.from(getStringListArg(_packagesArg)); - final Set excludedPlugins = - getStringListArg(_excludeArg).expand((String item) { - if (item.endsWith('.yaml')) { - final File file = packagesDir.fileSystem.file(item); - return (loadYaml(file.readAsStringSync()) as YamlList) - .toList() - .cast(); - } - return [item]; - }).toSet(); + final Set excludedPluginNames = _getExcludedPackageName(); final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); if (plugins.isEmpty && @@ -248,9 +286,9 @@ abstract class PluginCommand extends Command { in dir.list(followLinks: false)) { // A top-level Dart package is a plugin package. if (_isDartPackage(entity)) { - if (!excludedPlugins.contains(entity.basename) && - (plugins.isEmpty || plugins.contains(p.basename(entity.path)))) { - yield entity as Directory; + if (plugins.isEmpty || plugins.contains(p.basename(entity.path))) { + yield PackageEnumerationEntry(entity as Directory, + excluded: excludedPluginNames.contains(entity.basename)); } } else if (entity is Directory) { // Look for Dart packages under this top-level directory. @@ -264,13 +302,13 @@ abstract class PluginCommand extends Command { path.relative(subdir.path, from: dir.path); final String packageName = path.basename(subdir.path); final String basenamePath = path.basename(entity.path); - if (!excludedPlugins.contains(basenamePath) && - !excludedPlugins.contains(packageName) && - !excludedPlugins.contains(relativePath) && - (plugins.isEmpty || - plugins.contains(relativePath) || - plugins.contains(basenamePath))) { - yield subdir as Directory; + if (plugins.isEmpty || + plugins.contains(relativePath) || + plugins.contains(basenamePath)) { + yield PackageEnumerationEntry(subdir as Directory, + excluded: excludedPluginNames.contains(basenamePath) || + excludedPluginNames.contains(packageName) || + excludedPluginNames.contains(relativePath)); } } } @@ -279,27 +317,30 @@ abstract class PluginCommand extends Command { } } - /// Returns the example Dart package folders of the plugins involved in this - /// command execution. - Stream getExamples() => - getPlugins().expand(getExamplesForPlugin); - - /// Returns all Dart package folders (typically, plugin + example) of the - /// plugins involved in this command execution. - Stream getPackages() async* { - await for (final Directory plugin in getPlugins()) { + /// Returns all Dart package folders (typically, base package + example) of + /// the packages involved in this command execution. + /// + /// By default, packages excluded via --exclude will not be in the stream, but + /// they can be included by passing false for [filterExcluded]. + Stream getTargetPackagesAndSubpackages( + {bool filterExcluded = true}) async* { + await for (final PackageEnumerationEntry plugin + in getTargetPackages(filterExcluded: filterExcluded)) { yield plugin; - yield* plugin + yield* plugin.directory .list(recursive: true, followLinks: false) .where(_isDartPackage) - .cast(); + .map((FileSystemEntity directory) => PackageEnumerationEntry( + directory as Directory, // _isDartPackage guarantees this works. + excluded: plugin.excluded)); } } /// Returns the files contained, recursively, within the plugins /// involved in this command execution. Stream getFiles() { - return getPlugins() + return getTargetPackages() + .map((PackageEnumerationEntry entry) => entry.directory) .asyncExpand((Directory folder) => getFilesForPackage(folder)); } diff --git a/script/tool/lib/src/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_plugins_app_command.dart index ed7014456086..d4eccb8a313e 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -156,13 +156,14 @@ class CreateAllPluginsAppCommand extends PluginCommand { final Map pathDependencies = {}; - await for (final Directory package in getPlugins()) { - final String pluginName = package.basename; - final File pubspecFile = package.childFile('pubspec.yaml'); + await for (final PackageEnumerationEntry package in getTargetPackages()) { + final Directory pluginDirectory = package.directory; + final String pluginName = pluginDirectory.basename; + final File pubspecFile = pluginDirectory.childFile('pubspec.yaml'); final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); if (pubspec.publishTo != 'none') { - pathDependencies[pluginName] = PathDependency(package.path); + pathDependencies[pluginName] = PathDependency(pluginDirectory.path); } } return pathDependencies; diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index 20f01ff98f0e..29a8ceb12782 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_command.dart @@ -39,18 +39,23 @@ class ListCommand extends PluginCommand { Future run() async { switch (getStringArg(_type)) { case _plugin: - await for (final Directory package in getPlugins()) { - print(package.path); + await for (final PackageEnumerationEntry package + in getTargetPackages()) { + print(package.directory.path); } break; case _example: - await for (final Directory package in getExamples()) { + final Stream examples = getTargetPackages() + .map((PackageEnumerationEntry entry) => entry.directory) + .expand(getExamplesForPlugin); + await for (final Directory package in examples) { print(package.path); } break; case _package: - await for (final Directory package in getPackages()) { - print(package.path); + await for (final PackageEnumerationEntry package + in getTargetPackagesAndSubpackages()) { + print(package.directory.path); } break; case _file: diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 542e91af6431..00e64ddc21fe 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -185,6 +185,28 @@ void main() { package.childDirectory('example').path, ])); }); + + test('excludes subpackages when main package is excluded', () async { + final Directory excluded = createFakePlugin('a_plugin', packagesDir, + examples: ['example1', 'example2']); + final Directory included = createFakePackage('a_package', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(includeSubpackages: true); + await runCommand(command, arguments: ['--exclude=a_plugin']); + + expect( + command.checkedPackages, + unorderedEquals([ + included.path, + included.childDirectory('example').path, + ])); + expect(command.checkedPackages, isNot(contains(excluded.path))); + expect(command.checkedPackages, + isNot(contains(excluded.childDirectory('example1').path))); + expect(command.checkedPackages, + isNot(contains(excluded.childDirectory('example2').path))); + }); }); group('output', () { @@ -376,6 +398,23 @@ void main() { ])); }); + test('logs exclusions', () async { + createFakePackage('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = + await runCommand(command, arguments: ['--exclude=package_b']); + + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startSkipColor}Not running for package_b; excluded$_endColor', + ])); + }); + test('logs warnings', () async { final Directory warnPackage = createFakePackage('package_a', packagesDir); warnPackage @@ -435,6 +474,24 @@ void main() { expect(output, isNot(contains(contains('package a - ran')))); }); + test('counts exclusions as skips in run summary', () async { + createFakePackage('package_a', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = + await runCommand(command, arguments: ['--exclude=package_a']); + + expect( + output, + containsAllInOrder([ + '------------------------------------------------------------', + 'Skipped 1 package(s)', + '\n', + '${_startSuccessColor}No issues found!$_endColor', + ])); + }); + test('prints long-form run summary for long-output commands', () async { final Directory warnPackage1 = createFakePackage('package_a', packagesDir); @@ -478,6 +535,25 @@ void main() { ])); }); + test('prints exclusions as skips in long-form run summary', () async { + createFakePackage('package_a', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: true); + final List output = + await runCommand(command, arguments: ['--exclude=package_a']); + + expect( + output, + containsAllInOrder([ + ' package_a - ${_startSkipColor}excluded$_endColor', + '', + 'Skipped 1 package(s)', + '\n', + '${_startSuccessColor}No issues found!$_endColor', + ])); + }); + test('handles warnings outside of runForPackage', () async { createFakePackage('package_a', packagesDir); diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 7f67acfb2df3..2f332aa8eb55 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -22,12 +22,12 @@ import 'plugin_command_test.mocks.dart'; @GenerateMocks([GitDir]) void main() { late RecordingProcessRunner processRunner; + late SamplePluginCommand command; late CommandRunner runner; late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; late Directory thirdPartyPackagesDir; - late List plugins; late List?> gitDirCommands; late String gitDiffResponse; @@ -53,9 +53,7 @@ void main() { return Future.value(mockProcessResult); }); processRunner = RecordingProcessRunner(); - plugins = []; - final SamplePluginCommand samplePluginCommand = SamplePluginCommand( - plugins, + command = SamplePluginCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, @@ -63,7 +61,7 @@ void main() { ); runner = CommandRunner('common_command', 'Test for common functionality'); - runner.addCommand(samplePluginCommand); + runner.addCommand(command); }); group('plugin iteration', () { @@ -71,7 +69,8 @@ void main() { final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample']); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('includes both plugins and packages', () async { @@ -81,7 +80,7 @@ void main() { final Directory package4 = createFakePackage('package4', packagesDir); await runCapturingPrint(runner, ['sample']); expect( - plugins, + command.plugins, unorderedEquals([ plugin1.path, plugin2.path, @@ -96,7 +95,7 @@ void main() { final Directory plugin3 = createFakePlugin('plugin3', thirdPartyPackagesDir); await runCapturingPrint(runner, ['sample']); - expect(plugins, + expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path, plugin3.path])); }); @@ -108,7 +107,7 @@ void main() { await runCapturingPrint( runner, ['sample', '--packages=plugin1,package4']); expect( - plugins, + command.plugins, unorderedEquals([ plugin1.path, package4.path, @@ -123,7 +122,7 @@ void main() { await runCapturingPrint( runner, ['sample', '--plugins=plugin1,package4']); expect( - plugins, + command.plugins, unorderedEquals([ plugin1.path, package4.path, @@ -138,7 +137,7 @@ void main() { '--packages=plugin1,plugin2', '--exclude=plugin1' ]); - expect(plugins, unorderedEquals([plugin2.path])); + expect(command.plugins, unorderedEquals([plugin2.path])); }); test('exclude packages when packages flag isn\'t specified', () async { @@ -146,7 +145,7 @@ void main() { createFakePlugin('plugin2', packagesDir); await runCapturingPrint( runner, ['sample', '--exclude=plugin1,plugin2']); - expect(plugins, unorderedEquals([])); + expect(command.plugins, unorderedEquals([])); }); test('exclude federated plugins when packages flag is specified', () async { @@ -157,7 +156,7 @@ void main() { '--packages=federated/plugin1,plugin2', '--exclude=federated/plugin1' ]); - expect(plugins, unorderedEquals([plugin2.path])); + expect(command.plugins, unorderedEquals([plugin2.path])); }); test('exclude entire federated plugins when packages flag is specified', @@ -169,7 +168,7 @@ void main() { '--packages=federated/plugin1,plugin2', '--exclude=federated' ]); - expect(plugins, unorderedEquals([plugin2.path])); + expect(command.plugins, unorderedEquals([plugin2.path])); }); test('exclude accepts config files', () async { @@ -182,7 +181,7 @@ void main() { '--packages=plugin1', '--exclude=${configFile.path}' ]); - expect(plugins, unorderedEquals([])); + expect(command.plugins, unorderedEquals([])); }); group('test run-on-changed-packages', () { @@ -195,7 +194,8 @@ void main() { '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test( @@ -210,7 +210,8 @@ void main() { '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if .cirrus.yml changes.', () async { @@ -226,7 +227,8 @@ packages/plugin1/CHANGELOG '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if .ci.yaml changes', () async { @@ -242,7 +244,8 @@ packages/plugin1/CHANGELOG '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if anything in .ci/ changes', @@ -259,7 +262,8 @@ packages/plugin1/CHANGELOG '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if anything in script changes.', @@ -276,7 +280,8 @@ packages/plugin1/CHANGELOG '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if the root analysis options change.', @@ -293,7 +298,8 @@ packages/plugin1/CHANGELOG '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('all plugins should be tested if formatting options change.', @@ -310,7 +316,8 @@ packages/plugin1/CHANGELOG '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('Only changed plugin should be tested.', () async { @@ -323,7 +330,7 @@ packages/plugin1/CHANGELOG '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path])); + expect(command.plugins, unorderedEquals([plugin1.path])); }); test('multiple files in one plugin should also test the plugin', @@ -340,7 +347,7 @@ packages/plugin1/ios/plugin1.m '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path])); + expect(command.plugins, unorderedEquals([plugin1.path])); }); test('multiple plugins changed should test all the changed plugins', @@ -358,7 +365,8 @@ packages/plugin2/ios/plugin2.m '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test( @@ -379,7 +387,31 @@ packages/plugin1/plugin1_web/plugin1_web.dart '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path])); + expect(command.plugins, unorderedEquals([plugin1.path])); + }); + + test( + 'changing one plugin in a federated group should include all plugins in the group', + () async { + gitDiffResponse = ''' +packages/plugin1/plugin1/plugin1.dart +'''; + final Directory plugin1 = + createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); + final Directory plugin2 = createFakePlugin('plugin1_platform_interface', + packagesDir.childDirectory('plugin1')); + final Directory plugin3 = createFakePlugin( + 'plugin1_web', packagesDir.childDirectory('plugin1')); + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect( + command.plugins, + unorderedEquals( + [plugin1.path, plugin2.path, plugin3.path])); }); test( @@ -401,7 +433,8 @@ packages/plugin3/plugin3.dart '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); }); test('--exclude flag works with --run-on-changed-packages', () async { @@ -421,15 +454,155 @@ packages/plugin3/plugin3.dart '--run-on-changed-packages' ]); - expect(plugins, unorderedEquals([plugin1.path])); + expect(command.plugins, unorderedEquals([plugin1.path])); }); }); }); + + group('sharding', () { + test('distributes evenly when evenly divisible', () async { + final List> expectedShards = >[ + [ + createFakePackage('package1', packagesDir), + createFakePackage('package2', packagesDir), + createFakePackage('package3', packagesDir), + ], + [ + createFakePackage('package4', packagesDir), + createFakePackage('package5', packagesDir), + createFakePackage('package6', packagesDir), + ], + [ + createFakePackage('package7', packagesDir), + createFakePackage('package8', packagesDir), + createFakePackage('package9', packagesDir), + ], + ]; + + for (int i = 0; i < expectedShards.length; ++i) { + final SamplePluginCommand localCommand = SamplePluginCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: MockGitDir(), + ); + final CommandRunner localRunner = + CommandRunner('common_command', 'Shard testing'); + localRunner.addCommand(localCommand); + + await runCapturingPrint(localRunner, [ + 'sample', + '--shardIndex=$i', + '--shardCount=3', + ]); + expect( + localCommand.plugins, + unorderedEquals(expectedShards[i] + .map((Directory package) => package.path) + .toList())); + } + }); + + test('distributes as evenly as possible when not evenly divisible', + () async { + final List> expectedShards = >[ + [ + createFakePackage('package1', packagesDir), + createFakePackage('package2', packagesDir), + createFakePackage('package3', packagesDir), + ], + [ + createFakePackage('package4', packagesDir), + createFakePackage('package5', packagesDir), + createFakePackage('package6', packagesDir), + ], + [ + createFakePackage('package7', packagesDir), + createFakePackage('package8', packagesDir), + ], + ]; + + for (int i = 0; i < expectedShards.length; ++i) { + final SamplePluginCommand localCommand = SamplePluginCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: MockGitDir(), + ); + final CommandRunner localRunner = + CommandRunner('common_command', 'Shard testing'); + localRunner.addCommand(localCommand); + + await runCapturingPrint(localRunner, [ + 'sample', + '--shardIndex=$i', + '--shardCount=3', + ]); + expect( + localCommand.plugins, + unorderedEquals(expectedShards[i] + .map((Directory package) => package.path) + .toList())); + } + }); + + // In CI (which is the use case for sharding) we often want to run muliple + // commands on the same set of packages, but the exclusion lists for those + // commands may be different. In those cases we still want all the commands + // to operate on a consistent set of plugins. + // + // E.g., some commands require running build-examples in a previous step; + // excluding some plugins from the later step shouldn't change what's tested + // in each shard, as it may no longer align with what was built. + test('counts excluded plugins when sharding', () async { + final List> expectedShards = >[ + [ + createFakePackage('package1', packagesDir), + createFakePackage('package2', packagesDir), + createFakePackage('package3', packagesDir), + ], + [ + createFakePackage('package4', packagesDir), + createFakePackage('package5', packagesDir), + createFakePackage('package6', packagesDir), + ], + [ + createFakePackage('package7', packagesDir), + ], + ]; + // These would be in the last shard, but are excluded. + createFakePackage('package8', packagesDir); + createFakePackage('package9', packagesDir); + + for (int i = 0; i < expectedShards.length; ++i) { + final SamplePluginCommand localCommand = SamplePluginCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: MockGitDir(), + ); + final CommandRunner localRunner = + CommandRunner('common_command', 'Shard testing'); + localRunner.addCommand(localCommand); + + await runCapturingPrint(localRunner, [ + 'sample', + '--shardIndex=$i', + '--shardCount=3', + '--exclude=package8,package9', + ]); + expect( + localCommand.plugins, + unorderedEquals(expectedShards[i] + .map((Directory package) => package.path) + .toList())); + } + }); + }); } class SamplePluginCommand extends PluginCommand { SamplePluginCommand( - this._plugins, Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), @@ -437,7 +610,7 @@ class SamplePluginCommand extends PluginCommand { }) : super(packagesDir, processRunner: processRunner, platform: platform, gitDir: gitDir); - final List _plugins; + final List plugins = []; @override final String name = 'sample'; @@ -447,8 +620,8 @@ class SamplePluginCommand extends PluginCommand { @override Future run() async { - await for (final Directory package in getPlugins()) { - _plugins.add(package.path); + await for (final PackageEnumerationEntry package in getTargetPackages()) { + plugins.add(package.directory.path); } } } From 60100908443fffee28df82735a9c4019cf549227 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 4 Aug 2021 06:45:01 -0700 Subject: [PATCH 052/123] Don't use 'flutter upgrade' on Cirrus (#4213) This command isn't intended for CI use, and is also slower due to downloading artifacts that will be immidately discarded. Cirrus portion of https://github.com/flutter/flutter/issues/86037 --- .cirrus.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 5e8425fc2437..f978cc729799 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -20,8 +20,12 @@ flutter_upgrade_template: &FLUTTER_UPGRADE_TEMPLATE - git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" - git fetch origin # Switch to the requested branch. - - flutter channel $CHANNEL - - flutter upgrade + - git checkout $CHANNEL + # Reset to upstream branch, rather than using pull, since the base image + # can sometimes be in a state where it has diverged from upstream (!). + - git reset --hard @{u} + # Run doctor to allow auditing of what version of Flutter the run is using. + - flutter doctor -v << : *TOOL_SETUP_TEMPLATE macos_template: &MACOS_TEMPLATE From 083b45e19d3bfbfbfca0b6059aed6c1124019854 Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Fri, 6 Aug 2021 20:35:04 +0300 Subject: [PATCH 053/123] [image_picker] Fix README example (#4220) --- packages/image_picker/image_picker/CHANGELOG.md | 4 ++++ packages/image_picker/image_picker/README.md | 4 +--- packages/image_picker/image_picker/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index bd0a7a06b4fc..f9c7640183d5 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.3+1 + +* Fixed README Example. + ## 0.8.3 * Move `ImagePickerFromLimitedGalleryUITests` to `RunnerUITests` target. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index 18fd96d890fd..7499c356f3aa 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -47,9 +47,7 @@ import 'package:image_picker/image_picker.dart'; // Capture a video final XFile? photo = await _picker.pickVideo(source: ImageSource.camera); // Pick multiple images - final List? images = await _picker.pickMultiImage(source: ImageSource.gallery); - // Pick multiple photos - final List? photos = await _picker.pickMultiImage(source: ImageSource.camera); + final List? images = await _picker.pickMultiImage(); ... ``` diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index f56250f53715..e67e79fbba14 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.3 +version: 0.8.3+1 environment: sdk: ">=2.12.0 <3.0.0" From c70465665790a885d4cdbd4b8bc4bd6544bc6d3c Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Sat, 7 Aug 2021 03:55:04 +0200 Subject: [PATCH 054/123] [camera_web] Add `onCameraResolutionChanged` implementation (#4217) --- .../example/integration_test/camera_web_test.dart | 6 +++--- packages/camera/camera_web/lib/src/camera_web.dart | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 1b540a50e48d..083a25dd06bb 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -1068,11 +1068,11 @@ void main() { await streamQueue.cancel(); }); - testWidgets('onCameraResolutionChanged throws UnimplementedError', + testWidgets('onCameraResolutionChanged emits an empty stream', (tester) async { expect( - () => CameraPlatform.instance.onCameraResolutionChanged(cameraId), - throwsUnimplementedError, + CameraPlatform.instance.onCameraResolutionChanged(cameraId), + emits(isEmpty), ); }); diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 35241d0c9b8b..dbfbcacd3ce0 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -302,9 +302,14 @@ class CameraPlugin extends CameraPlatform { return _cameraEvents(cameraId).whereType(); } + /// Emits an empty stream as there is no event corresponding to a change + /// in the camera resolution on the web. + /// + /// In order to change the camera resolution a new camera with appropriate + /// [CameraOptions.video] constraints has to be created and initialized. @override Stream onCameraResolutionChanged(int cameraId) { - throw UnimplementedError('onCameraResolutionChanged() is not implemented.'); + return const Stream.empty(); } @override From acc3202466a8b4c0b1bcb91fc1ce520c7b021e44 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Sat, 7 Aug 2021 04:00:05 +0200 Subject: [PATCH 055/123] [camera_web] Add support for device orientation (#4219) --- .../camera_settings_test.dart | 114 ++++++ .../integration_test/camera_web_test.dart | 349 ++++++++++++++++-- .../integration_test/helpers/mocks.dart | 8 + .../camera_web/lib/src/camera_settings.dart | 34 ++ .../camera/camera_web/lib/src/camera_web.dart | 63 +++- .../lib/src/types/camera_error_code.dart | 4 + .../lib/src/types/orientation_type.dart | 26 ++ .../camera_web/lib/src/types/types.dart | 2 +- .../test/types/camera_error_code_test.dart | 7 + 9 files changed, 576 insertions(+), 31 deletions(-) create mode 100644 packages/camera/camera_web/lib/src/types/orientation_type.dart diff --git a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart index 7e5119003129..bc228b2e35c6 100644 --- a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart @@ -607,6 +607,120 @@ void main() { ); }); }); + + group('mapDeviceOrientationToOrientationType', () { + testWidgets( + 'returns portraitPrimary ' + 'when the device orientation is portraitUp', (tester) async { + expect( + settings.mapDeviceOrientationToOrientationType( + DeviceOrientation.portraitUp, + ), + equals(OrientationType.portraitPrimary), + ); + }); + + testWidgets( + 'returns landscapePrimary ' + 'when the device orientation is landscapeLeft', (tester) async { + expect( + settings.mapDeviceOrientationToOrientationType( + DeviceOrientation.landscapeLeft, + ), + equals(OrientationType.landscapePrimary), + ); + }); + + testWidgets( + 'returns portraitSecondary ' + 'when the device orientation is portraitDown', (tester) async { + expect( + settings.mapDeviceOrientationToOrientationType( + DeviceOrientation.portraitDown, + ), + equals(OrientationType.portraitSecondary), + ); + }); + + testWidgets( + 'returns landscapeSecondary ' + 'when the device orientation is landscapeRight', (tester) async { + expect( + settings.mapDeviceOrientationToOrientationType( + DeviceOrientation.landscapeRight, + ), + equals(OrientationType.landscapeSecondary), + ); + }); + }); + + group('mapOrientationTypeToDeviceOrientation', () { + testWidgets( + 'returns portraitUp ' + 'when the orientation type is portraitPrimary', (tester) async { + expect( + settings.mapOrientationTypeToDeviceOrientation( + OrientationType.portraitPrimary, + ), + equals(DeviceOrientation.portraitUp), + ); + }); + + testWidgets( + 'returns landscapeLeft ' + 'when the orientation type is landscapePrimary', (tester) async { + expect( + settings.mapOrientationTypeToDeviceOrientation( + OrientationType.landscapePrimary, + ), + equals(DeviceOrientation.landscapeLeft), + ); + }); + + testWidgets( + 'returns portraitDown ' + 'when the orientation type is portraitSecondary', (tester) async { + expect( + settings.mapOrientationTypeToDeviceOrientation( + OrientationType.portraitSecondary, + ), + equals(DeviceOrientation.portraitDown), + ); + }); + + testWidgets( + 'returns portraitDown ' + 'when the orientation type is portraitSecondary', (tester) async { + expect( + settings.mapOrientationTypeToDeviceOrientation( + OrientationType.portraitSecondary, + ), + equals(DeviceOrientation.portraitDown), + ); + }); + + testWidgets( + 'returns landscapeRight ' + 'when the orientation type is landscapeSecondary', (tester) async { + expect( + settings.mapOrientationTypeToDeviceOrientation( + OrientationType.landscapeSecondary, + ), + equals(DeviceOrientation.landscapeRight), + ); + }); + + testWidgets( + 'returns portraitUp ' + 'for an unknown orientation type', (tester) async { + expect( + settings.mapOrientationTypeToDeviceOrientation( + 'unknown', + ), + equals(DeviceOrientation.portraitUp), + ); + }); + }); }); } diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 083a25dd06bb..e11634d83fce 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -30,6 +30,11 @@ void main() { late Navigator navigator; late MediaDevices mediaDevices; late VideoElement videoElement; + late Screen screen; + late ScreenOrientation screenOrientation; + late Document document; + late Element documentElement; + late CameraSettings cameraSettings; setUp(() async { @@ -39,11 +44,23 @@ void main() { videoElement = getVideoElementWithBlankStream(Size(10, 10)); - cameraSettings = MockCameraSettings(); - when(() => window.navigator).thenReturn(navigator); when(() => navigator.mediaDevices).thenReturn(mediaDevices); + screen = MockScreen(); + screenOrientation = MockScreenOrientation(); + + when(() => screen.orientation).thenReturn(screenOrientation); + when(() => window.screen).thenReturn(screen); + + document = MockDocument(); + documentElement = MockElement(); + + when(() => document.documentElement).thenReturn(documentElement); + when(() => window.document).thenReturn(document); + + cameraSettings = MockCameraSettings(); + when( () => cameraSettings.getMediaStreamForOptions( any(), @@ -636,23 +653,236 @@ void main() { }); }); - testWidgets('lockCaptureOrientation throws UnimplementedError', - (tester) async { - expect( - () => CameraPlatform.instance.lockCaptureOrientation( + group('lockCaptureOrientation', () { + setUp(() { + when( + () => cameraSettings.mapDeviceOrientationToOrientationType(any()), + ).thenReturn(OrientationType.portraitPrimary); + }); + + testWidgets( + 'requests full-screen mode ' + 'on documentElement', (tester) async { + await CameraPlatform.instance.lockCaptureOrientation( cameraId, - DeviceOrientation.landscapeLeft, - ), - throwsUnimplementedError, - ); + DeviceOrientation.portraitUp, + ); + + verify(documentElement.requestFullscreen).called(1); + }); + + testWidgets( + 'locks the capture orientation ' + 'based on the given device orientation', (tester) async { + when( + () => cameraSettings.mapDeviceOrientationToOrientationType( + DeviceOrientation.landscapeRight, + ), + ).thenReturn(OrientationType.landscapeSecondary); + + await CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.landscapeRight, + ); + + verify( + () => cameraSettings.mapDeviceOrientationToOrientationType( + DeviceOrientation.landscapeRight, + ), + ).called(1); + + verify( + () => screenOrientation.lock( + OrientationType.landscapeSecondary, + ), + ).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with orientationNotSupported error ' + 'when screen is not supported', (tester) async { + when(() => window.screen).thenReturn(null); + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitUp, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets( + 'with orientationNotSupported error ' + 'when screen orientation is not supported', (tester) async { + when(() => screen.orientation).thenReturn(null); + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitUp, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets( + 'with orientationNotSupported error ' + 'when documentElement is not available', (tester) async { + when(() => document.documentElement).thenReturn(null); + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitUp, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets('when lock throws DomException', (tester) async { + final exception = FakeDomException(DomException.NOT_ALLOWED); + + when(() => screenOrientation.lock(any())).thenThrow(exception); + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitDown, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); }); - testWidgets('unlockCaptureOrientation throws UnimplementedError', - (tester) async { - expect( - () => CameraPlatform.instance.unlockCaptureOrientation(cameraId), - throwsUnimplementedError, - ); + group('unlockCaptureOrientation', () { + setUp(() { + when( + () => cameraSettings.mapDeviceOrientationToOrientationType(any()), + ).thenReturn(OrientationType.portraitPrimary); + }); + + testWidgets( + 'requests full-screen mode ' + 'on documentElement', (tester) async { + await CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ); + + verify(documentElement.requestFullscreen).called(1); + }); + + testWidgets('unlocks the capture orientation', (tester) async { + await CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ); + + verify(screenOrientation.unlock).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with orientationNotSupported error ' + 'when screen is not supported', (tester) async { + when(() => window.screen).thenReturn(null); + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets( + 'with orientationNotSupported error ' + 'when screen orientation is not supported', (tester) async { + when(() => screen.orientation).thenReturn(null); + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets( + 'with orientationNotSupported error ' + 'when documentElement is not available', (tester) async { + when(() => document.documentElement).thenReturn(null); + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets('when unlock throws DomException', (tester) async { + final exception = FakeDomException(DomException.NOT_ALLOWED); + + when(screenOrientation.unlock).thenThrow(exception); + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); }); group('takePicture', () { @@ -1213,12 +1443,87 @@ void main() { ); }); - testWidgets('onDeviceOrientationChanged throws UnimplementedError', - (tester) async { - expect( - () => CameraPlatform.instance.onDeviceOrientationChanged(), - throwsUnimplementedError, - ); + group('onDeviceOrientationChanged', () { + group('emits an empty stream', () { + testWidgets('when screen is not supported', (tester) async { + when(() => window.screen).thenReturn(null); + + expect( + CameraPlatform.instance.onDeviceOrientationChanged(), + emits(isEmpty), + ); + }); + + testWidgets('when screen orientation is not supported', + (tester) async { + when(() => screen.orientation).thenReturn(null); + + expect( + CameraPlatform.instance.onDeviceOrientationChanged(), + emits(isEmpty), + ); + }); + }); + + testWidgets( + 'emits a DeviceOrientationChangedEvent ' + 'when the screen orientation is changed', (tester) async { + when( + () => cameraSettings.mapOrientationTypeToDeviceOrientation( + OrientationType.landscapePrimary, + ), + ).thenReturn(DeviceOrientation.landscapeLeft); + + when( + () => cameraSettings.mapOrientationTypeToDeviceOrientation( + OrientationType.portraitSecondary, + ), + ).thenReturn(DeviceOrientation.portraitDown); + + final eventStreamController = StreamController(); + + when(() => screenOrientation.onChange) + .thenAnswer((_) => eventStreamController.stream); + + final Stream eventStream = + CameraPlatform.instance.onDeviceOrientationChanged(); + + final streamQueue = StreamQueue(eventStream); + + // Change the screen orientation to landscapePrimary and + // emit an event on the screenOrientation.onChange stream. + when(() => screenOrientation.type) + .thenReturn(OrientationType.landscapePrimary); + + eventStreamController.add(Event('orientationChanged')); + + expect( + await streamQueue.next, + equals( + DeviceOrientationChangedEvent( + DeviceOrientation.landscapeLeft, + ), + ), + ); + + // Change the screen orientation to portraitSecondary and + // emit an event on the screenOrientation.onChange stream. + when(() => screenOrientation.type) + .thenReturn(OrientationType.portraitSecondary); + + eventStreamController.add(Event('orientationChanged')); + + expect( + await streamQueue.next, + equals( + DeviceOrientationChangedEvent( + DeviceOrientation.portraitDown, + ), + ), + ); + + await streamQueue.cancel(); + }); }); }); }); diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart index 8af3a9c3cd81..5fa52dd3398d 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -14,6 +14,14 @@ import 'package:mocktail/mocktail.dart'; class MockWindow extends Mock implements Window {} +class MockScreen extends Mock implements Screen {} + +class MockScreenOrientation extends Mock implements ScreenOrientation {} + +class MockDocument extends Mock implements Document {} + +class MockElement extends Mock implements Element {} + class MockNavigator extends Mock implements Navigator {} class MockMediaDevices extends Mock implements MediaDevices {} diff --git a/packages/camera/camera_web/lib/src/camera_settings.dart b/packages/camera/camera_web/lib/src/camera_settings.dart index 1412248a2371..ce713bc52468 100644 --- a/packages/camera/camera_web/lib/src/camera_settings.dart +++ b/packages/camera/camera_web/lib/src/camera_settings.dart @@ -230,4 +230,38 @@ class CameraSettings { return Size(320, 240); } } + + /// Maps the given [deviceOrientation] to [OrientationType]. + String mapDeviceOrientationToOrientationType( + DeviceOrientation deviceOrientation, + ) { + switch (deviceOrientation) { + case DeviceOrientation.portraitUp: + return OrientationType.portraitPrimary; + case DeviceOrientation.landscapeLeft: + return OrientationType.landscapePrimary; + case DeviceOrientation.portraitDown: + return OrientationType.portraitSecondary; + case DeviceOrientation.landscapeRight: + return OrientationType.landscapeSecondary; + } + } + + /// Maps the given [orientationType] to [DeviceOrientation]. + DeviceOrientation mapOrientationTypeToDeviceOrientation( + String orientationType, + ) { + switch (orientationType) { + case OrientationType.portraitPrimary: + return DeviceOrientation.portraitUp; + case OrientationType.landscapePrimary: + return DeviceOrientation.landscapeLeft; + case OrientationType.portraitSecondary: + return DeviceOrientation.portraitDown; + case OrientationType.landscapeSecondary: + return DeviceOrientation.landscapeRight; + default: + return DeviceOrientation.portraitUp; + } + } } diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index dbfbcacd3ce0..01fc0a23aa34 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -329,22 +329,69 @@ class CameraPlugin extends CameraPlatform { @override Stream onDeviceOrientationChanged() { - throw UnimplementedError( - 'onDeviceOrientationChanged() is not implemented.', - ); + final orientation = window?.screen?.orientation; + + if (orientation != null) { + return orientation.onChange.map( + (html.Event _) { + final deviceOrientation = _cameraSettings + .mapOrientationTypeToDeviceOrientation(orientation.type!); + return DeviceOrientationChangedEvent(deviceOrientation); + }, + ); + } else { + return const Stream.empty(); + } } @override Future lockCaptureOrientation( int cameraId, - DeviceOrientation orientation, - ) { - throw UnimplementedError('lockCaptureOrientation() is not implemented.'); + DeviceOrientation deviceOrientation, + ) async { + try { + final orientation = window?.screen?.orientation; + final documentElement = window?.document.documentElement; + + if (orientation != null && documentElement != null) { + final orientationType = _cameraSettings + .mapDeviceOrientationToOrientationType(deviceOrientation); + + // Full-screen mode may be required to modify the device orientation. + // See: https://w3c.github.io/screen-orientation/#interaction-with-fullscreen-api + documentElement.requestFullscreen(); + await orientation.lock(orientationType.toString()); + } else { + throw PlatformException( + code: CameraErrorCode.orientationNotSupported.toString(), + message: 'Orientation is not supported in the current browser.', + ); + } + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } } @override - Future unlockCaptureOrientation(int cameraId) { - throw UnimplementedError('unlockCaptureOrientation() is not implemented.'); + Future unlockCaptureOrientation(int cameraId) async { + try { + final orientation = window?.screen?.orientation; + final documentElement = window?.document.documentElement; + + if (orientation != null && documentElement != null) { + // Full-screen mode may be required to modify the device orientation. + // See: https://w3c.github.io/screen-orientation/#interaction-with-fullscreen-api + documentElement.requestFullscreen(); + orientation.unlock(); + } else { + throw PlatformException( + code: CameraErrorCode.orientationNotSupported.toString(), + message: 'Orientation is not supported in the current browser.', + ); + } + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } } @override diff --git a/packages/camera/camera_web/lib/src/types/camera_error_code.dart b/packages/camera/camera_web/lib/src/types/camera_error_code.dart index 3dcace3ca2d6..9a70663c4aaf 100644 --- a/packages/camera/camera_web/lib/src/types/camera_error_code.dart +++ b/packages/camera/camera_web/lib/src/types/camera_error_code.dart @@ -48,6 +48,10 @@ class CameraErrorCode { static const CameraErrorCode missingMetadata = CameraErrorCode._('cameraMissingMetadata'); + /// The camera orientation is not supported. + static const CameraErrorCode orientationNotSupported = + CameraErrorCode._('orientationNotSupported'); + /// An unknown camera error. static const CameraErrorCode unknown = CameraErrorCode._('cameraUnknown'); diff --git a/packages/camera/camera_web/lib/src/types/orientation_type.dart b/packages/camera/camera_web/lib/src/types/orientation_type.dart new file mode 100644 index 000000000000..717f5f399541 --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/orientation_type.dart @@ -0,0 +1,26 @@ +// 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:flutter/services.dart'; + +/// A screen orientation type. +/// +/// See: https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation/type +abstract class OrientationType { + /// The primary portrait mode orientation. + /// Corresponds to [DeviceOrientation.portraitUp]. + static const String portraitPrimary = 'portrait-primary'; + + /// The secondary portrait mode orientation. + /// Corresponds to [DeviceOrientation.portraitSecondary]. + static const String portraitSecondary = 'portrait-secondary'; + + /// The primary landscape mode orientation. + /// Corresponds to [DeviceOrientation.landscapeLeft]. + static const String landscapePrimary = 'landscape-primary'; + + /// The secondary landscape mode orientation. + /// Corresponds to [DeviceOrientation.landscapeRight]. + static const String landscapeSecondary = 'landscape-secondary'; +} diff --git a/packages/camera/camera_web/lib/src/types/types.dart b/packages/camera/camera_web/lib/src/types/types.dart index 788ec79de205..4e3902fcb3ee 100644 --- a/packages/camera/camera_web/lib/src/types/types.dart +++ b/packages/camera/camera_web/lib/src/types/types.dart @@ -1,9 +1,9 @@ // 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. - export 'camera_error_code.dart'; export 'camera_metadata.dart'; export 'camera_options.dart'; export 'camera_web_exception.dart'; export 'media_device_kind.dart'; +export 'orientation_type.dart'; diff --git a/packages/camera/camera_web/test/types/camera_error_code_test.dart b/packages/camera/camera_web/test/types/camera_error_code_test.dart index ca896e8696d7..6f2d7dd1cd09 100644 --- a/packages/camera/camera_web/test/types/camera_error_code_test.dart +++ b/packages/camera/camera_web/test/types/camera_error_code_test.dart @@ -75,6 +75,13 @@ void main() { ); }); + test('orientationNotSupported', () { + expect( + CameraErrorCode.orientationNotSupported.toString(), + equals('orientationNotSupported'), + ); + }); + test('unknown', () { expect( CameraErrorCode.unknown.toString(), From b9512b627343f58ab543396b7bfed27a8add5a4e Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Mon, 9 Aug 2021 19:20:05 +0200 Subject: [PATCH 056/123] [camera_web] Add support for a flash mode (#4222) --- .../example/integration_test/camera_test.dart | 396 +++++++++++++++++- .../integration_test/camera_web_test.dart | 127 +++++- .../camera/camera_web/lib/src/camera.dart | 98 ++++- .../camera/camera_web/lib/src/camera_web.dart | 11 +- .../lib/src/types/camera_error_code.dart | 8 + .../test/types/camera_error_code_test.dart | 14 + 6 files changed, 619 insertions(+), 35 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 49690ed38ab5..740e24f87819 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -5,6 +5,7 @@ import 'dart:html'; import 'dart:ui'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_settings.dart'; import 'package:camera_web/src/types/types.dart'; @@ -18,10 +19,23 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('Camera', () { + const textureId = 1; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + late MediaStream mediaStream; late CameraSettings cameraSettings; setUp(() { + window = MockWindow(); + navigator = MockNavigator(); + mediaDevices = MockMediaDevices(); + + when(() => window.navigator).thenReturn(navigator); + when(() => navigator.mediaDevices).thenReturn(mediaDevices); + cameraSettings = MockCameraSettings(); final videoElement = getVideoElementWithBlankStream(Size(10, 10)); @@ -51,7 +65,7 @@ void main() { ); final camera = Camera( - textureId: 1, + textureId: textureId, options: options, cameraSettings: cameraSettings, ); @@ -61,7 +75,7 @@ void main() { verify( () => cameraSettings.getMediaStreamForOptions( options, - cameraId: 1, + cameraId: textureId, ), ).called(1); }); @@ -72,7 +86,7 @@ void main() { const audioConstraints = AudioConstraints(enabled: true); final camera = Camera( - textureId: 1, + textureId: textureId, options: CameraOptions( audio: audioConstraints, ), @@ -100,7 +114,7 @@ void main() { 'creates a wrapping div element ' 'with correct properties', (tester) async { final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); @@ -111,6 +125,17 @@ void main() { expect(camera.divElement.children, contains(camera.videoElement)); }); + testWidgets('initializes the camera stream', (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ); + + await camera.initialize(); + + expect(camera.stream, mediaStream); + }); + testWidgets( 'throws an exception ' 'when CameraSettings.getMediaStreamForOptions throws', @@ -121,7 +146,7 @@ void main() { cameraId: any(named: 'cameraId'))).thenThrow(exception); final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); @@ -137,7 +162,7 @@ void main() { var startedPlaying = false; final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); @@ -154,9 +179,8 @@ void main() { }); testWidgets( - 'assigns a media stream ' + 'initializes the camera stream ' 'from CameraSettings.getMediaStreamForOptions ' - 'to the video element\'s source ' 'if it does not exist', (tester) async { final options = CameraOptions( video: VideoConstraints( @@ -165,7 +189,7 @@ void main() { ); final camera = Camera( - textureId: 1, + textureId: textureId, options: options, cameraSettings: cameraSettings, ); @@ -182,18 +206,19 @@ void main() { verify( () => cameraSettings.getMediaStreamForOptions( options, - cameraId: 1, + cameraId: textureId, ), ).called(2); expect(camera.videoElement.srcObject, mediaStream); + expect(camera.stream, mediaStream); }); }); group('stop', () { - testWidgets('resets the video element\'s source', (tester) async { + testWidgets('resets the camera stream', (tester) async { final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); @@ -203,13 +228,14 @@ void main() { camera.stop(); expect(camera.videoElement.srcObject, isNull); + expect(camera.stream, isNull); }); }); group('takePicture', () { testWidgets('returns a captured picture', (tester) async { final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); @@ -220,6 +246,99 @@ void main() { expect(pictureFile, isNotNull); }); + + group( + 'enables the torch mode ' + 'when taking a picture', () { + late List videoTracks; + late MediaStream videoStream; + late VideoElement videoElement; + + setUp(() { + videoTracks = [MockMediaStreamTrack(), MockMediaStreamTrack()]; + videoStream = FakeMediaStream(videoTracks); + + videoElement = getVideoElementWithBlankStream(Size(100, 100)) + ..muted = true; + + when(() => videoTracks.first.applyConstraints(any())) + .thenAnswer((_) async => {}); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + }); + + testWidgets('if the flash mode is auto', (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream + ..videoElement = videoElement + ..flashMode = FlashMode.auto; + + await camera.play(); + + final _ = await camera.takePicture(); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": true, + } + ] + }), + ).called(1); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": false, + } + ] + }), + ).called(1); + }); + + testWidgets('if the flash mode is always', (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream + ..videoElement = videoElement + ..flashMode = FlashMode.always; + + await camera.play(); + + final _ = await camera.takePicture(); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": true, + } + ] + }), + ).called(1); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": false, + } + ] + }), + ).called(1); + }); + }); }); group('getVideoSize', () { @@ -232,7 +351,7 @@ void main() { mediaStream = videoElement.captureStream(); final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); @@ -252,7 +371,7 @@ void main() { mediaStream = videoElement.captureStream(); final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); @@ -265,10 +384,251 @@ void main() { }); }); + group('setFlashMode', () { + late List videoTracks; + late MediaStream videoStream; + + setUp(() { + videoTracks = [MockMediaStreamTrack(), MockMediaStreamTrack()]; + videoStream = FakeMediaStream(videoTracks); + + when(() => videoTracks.first.applyConstraints(any())) + .thenAnswer((_) async => {}); + + when(videoTracks.first.getCapabilities).thenReturn({}); + }); + + testWidgets('sets the camera flash mode', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream; + + const flashMode = FlashMode.always; + + camera.setFlashMode(flashMode); + + expect( + camera.flashMode, + equals(flashMode), + ); + }); + + testWidgets( + 'enables the torch mode ' + 'if the flash mode is torch', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream; + + camera.setFlashMode(FlashMode.torch); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": true, + } + ] + }), + ).called(1); + }); + + testWidgets( + 'disables the torch mode ' + 'if the flash mode is not torch', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream; + + camera.setFlashMode(FlashMode.auto); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": false, + } + ] + }), + ).called(1); + }); + + group('throws CameraWebException', () { + testWidgets( + 'with torchModeNotSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream; + + expect( + () => camera.setFlashMode(FlashMode.always), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.torchModeNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with torchModeNotSupported error ' + 'when the torch mode is not supported ' + 'in the browser', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': false, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream; + + expect( + () => camera.setFlashMode(FlashMode.always), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.torchModeNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with torchModeNotSupported error ' + 'when the torch mode is not supported ' + 'by the camera', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': false, + }); + + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream; + + expect( + () => camera.setFlashMode(FlashMode.always), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.torchModeNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with notStarted error ' + 'when the camera stream has not been initialized', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + )..window = window; + + expect( + () => camera.setFlashMode(FlashMode.always), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.notStarted, + ), + ), + ); + }); + }); + }); + group('getViewType', () { testWidgets('returns a correct view type', (tester) async { - const textureId = 1; - final camera = Camera( textureId: textureId, cameraSettings: cameraSettings, @@ -286,7 +646,7 @@ void main() { group('dispose', () { testWidgets('resets the video element\'s source', (tester) async { final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index e11634d83fce..fda35dd088c1 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -78,6 +78,7 @@ void main() { setUpAll(() { registerFallbackValue(MockMediaStreamTrack()); registerFallbackValue(MockCameraOptions()); + registerFallbackValue(FlashMode.off); }); testWidgets('CameraPlugin is the live instance', (tester) async { @@ -981,14 +982,79 @@ void main() { ); }); - testWidgets('setFlashMode throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.setFlashMode( - cameraId, - FlashMode.auto, - ), - throwsUnimplementedError, - ); + group('setFlashMode', () { + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.setFlashMode( + cameraId, + FlashMode.always, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when setFlashMode throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(() => camera.setFlashMode(any())).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.setFlashMode( + cameraId, + FlashMode.always, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when setFlashMode throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(() => camera.setFlashMode(any())).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.setFlashMode( + cameraId, + FlashMode.torch, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); }); testWidgets('setExposureMode throws UnimplementedError', (tester) async { @@ -1345,7 +1411,7 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on video error ' + 'on initialize video error ' 'with a message', (tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); @@ -1380,7 +1446,7 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on video error ' + 'on initialize video error ' 'with no message', (tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); @@ -1411,7 +1477,7 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on abort error', (tester) async { + 'on initialize abort error', (tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); @@ -1433,6 +1499,45 @@ void main() { await streamQueue.cancel(); }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on setFlashMode error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(() => camera.setFlashMode(any())).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.setFlashMode( + cameraId, + FlashMode.always, + ), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); }); testWidgets('onVideoRecordedEvent throws UnimplementedError', diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 9e469033dfc4..1cd007b917bb 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -8,6 +8,7 @@ import 'dart:ui'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera_settings.dart'; import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/foundation.dart'; import 'shims/dart_ui.dart' as ui; @@ -39,6 +40,10 @@ class Camera { this.options = const CameraOptions(), }) : _cameraSettings = cameraSettings; + // A torch mode constraint name. + // See: https://w3c.github.io/mediacapture-image/#dom-mediatracksupportedconstraints-torch + static const _torchModeKey = "torch"; + /// The texture id used to register the camera view. final int textureId; @@ -47,20 +52,32 @@ class Camera { /// The video element that displays the camera stream. /// Initialized in [initialize]. - late html.VideoElement videoElement; + late final html.VideoElement videoElement; /// The wrapping element for the [videoElement] to avoid overriding /// the custom styles applied in [_applyDefaultVideoStyles]. /// Initialized in [initialize]. - late html.DivElement divElement; + late final html.DivElement divElement; + + /// The camera stream displayed in the [videoElement]. + /// Initialized in [initialize] and [play], reset in [stop]. + html.MediaStream? stream; + + /// The camera flash mode. + @visibleForTesting + FlashMode? flashMode; /// The camera settings used to get the media stream for the camera. final CameraSettings _cameraSettings; + /// The current browser window used to access media devices. + @visibleForTesting + html.Window? window = html.window; + /// Initializes the camera stream displayed in the [videoElement]. /// Registers the camera view with [textureId] under [_getViewType] type. Future initialize() async { - final stream = await _cameraSettings.getMediaStreamForOptions( + stream = await _cameraSettings.getMediaStreamForOptions( options, cameraId: textureId, ); @@ -89,7 +106,7 @@ class Camera { /// Initializes the camera source if the camera was previously stopped. Future play() async { if (videoElement.srcObject == null) { - final stream = await _cameraSettings.getMediaStreamForOptions( + stream = await _cameraSettings.getMediaStreamForOptions( options, cameraId: textureId, ); @@ -107,18 +124,36 @@ class Camera { } } videoElement.srcObject = null; + stream = null; } /// Captures a picture and returns the saved file in a JPEG format. + /// + /// Enables the device flash when taking a picture if the flash mode + /// is either [FlashMode.auto] or [FlashMode.always]. Future takePicture() async { + final shouldEnableTorchMode = + flashMode == FlashMode.auto || flashMode == FlashMode.always; + + if (shouldEnableTorchMode) { + _setTorchMode(enabled: true); + } + final videoWidth = videoElement.videoWidth; final videoHeight = videoElement.videoHeight; final canvas = html.CanvasElement(width: videoWidth, height: videoHeight); + canvas.context2D ..translate(videoWidth, 0) ..scale(-1, 1) ..drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight); + final blob = await canvas.toBlob('image/jpeg'); + + if (shouldEnableTorchMode) { + _setTorchMode(enabled: false); + } + return XFile(html.Url.createObjectUrl(blob)); } @@ -146,6 +181,61 @@ class Camera { } } + /// Sets the camera flash mode to [mode]. + void setFlashMode(FlashMode mode) { + final mediaDevices = window?.navigator.mediaDevices; + final supportedConstraints = mediaDevices?.getSupportedConstraints(); + final torchModeSupported = supportedConstraints?[_torchModeKey] ?? false; + + if (!torchModeSupported) { + throw CameraWebException( + textureId, + CameraErrorCode.torchModeNotSupported, + 'The torch mode is not supported in the current browser.', + ); + } + + // Save the updated flash mode to be used later when taking a picture. + flashMode = mode; + + // Enable the torch mode only if the flash mode is torch. + _setTorchMode(enabled: mode == FlashMode.torch); + } + + /// Sets the camera torch mode constraint to [enabled]. + void _setTorchMode({required bool enabled}) { + final videoTracks = stream?.getVideoTracks() ?? []; + + if (videoTracks.isNotEmpty) { + final defaultVideoTrack = videoTracks.first; + + final bool canEnableTorchMode = + defaultVideoTrack.getCapabilities()[_torchModeKey] ?? false; + + if (canEnableTorchMode) { + defaultVideoTrack.applyConstraints({ + "advanced": [ + { + _torchModeKey: enabled, + } + ] + }); + } else { + throw CameraWebException( + textureId, + CameraErrorCode.torchModeNotSupported, + 'The torch mode is not supported by the current camera.', + ); + } + } else { + throw CameraWebException( + textureId, + CameraErrorCode.notStarted, + 'The camera has not been initialized or started.', + ); + } + } + /// Returns the registered view type of the camera. String getViewType() => _getViewType(textureId); diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 01fc0a23aa34..0ae0d9e75c24 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -429,8 +429,15 @@ class CameraPlugin extends CameraPlatform { } @override - Future setFlashMode(int cameraId, FlashMode mode) { - throw UnimplementedError('setFlashMode() is not implemented.'); + Future setFlashMode(int cameraId, FlashMode mode) async { + try { + getCamera(cameraId).setFlashMode(mode); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } } @override diff --git a/packages/camera/camera_web/lib/src/types/camera_error_code.dart b/packages/camera/camera_web/lib/src/types/camera_error_code.dart index 9a70663c4aaf..904920db6ac6 100644 --- a/packages/camera/camera_web/lib/src/types/camera_error_code.dart +++ b/packages/camera/camera_web/lib/src/types/camera_error_code.dart @@ -52,6 +52,14 @@ class CameraErrorCode { static const CameraErrorCode orientationNotSupported = CameraErrorCode._('orientationNotSupported'); + /// The camera torch mode is not supported. + static const CameraErrorCode torchModeNotSupported = + CameraErrorCode._('torchModeNotSupported'); + + /// The camera has not been initialized or started. + static const CameraErrorCode notStarted = + CameraErrorCode._('cameraNotStarted'); + /// An unknown camera error. static const CameraErrorCode unknown = CameraErrorCode._('cameraUnknown'); diff --git a/packages/camera/camera_web/test/types/camera_error_code_test.dart b/packages/camera/camera_web/test/types/camera_error_code_test.dart index 6f2d7dd1cd09..1fec82b16f8d 100644 --- a/packages/camera/camera_web/test/types/camera_error_code_test.dart +++ b/packages/camera/camera_web/test/types/camera_error_code_test.dart @@ -82,6 +82,20 @@ void main() { ); }); + test('torchModeNotSupported', () { + expect( + CameraErrorCode.torchModeNotSupported.toString(), + equals('torchModeNotSupported'), + ); + }); + + test('notStarted', () { + expect( + CameraErrorCode.notStarted.toString(), + equals('cameraNotStarted'), + ); + }); + test('unknown', () { expect( CameraErrorCode.unknown.toString(), From 19f2ff71a8ba86b4cf4a52e9d18b4c650dcb9285 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Mon, 9 Aug 2021 12:40:05 -0700 Subject: [PATCH 057/123] Add `buildViewWithTextDirection` to platform interface (#4121) --- .../CHANGELOG.md | 4 + .../method_channel_google_maps_flutter.dart | 160 ++++++++---------- .../google_maps_flutter_platform.dart | 38 ++++- .../pubspec.yaml | 2 +- .../google_maps_flutter_platform_test.dart | 40 +++++ 5 files changed, 157 insertions(+), 87 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md index 2dc533fe1dfa..5d361d8e0c7c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.1 + +* Method `buildViewWithTextDirection` has been added to the platform interface. + ## 2.1.0 * Add support for Hybrid Composition when building the Google Maps widget on Android. Set diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart index 41aedc759b15..2b9c71ee85bd 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart @@ -456,11 +456,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { /// Defaults to false. bool useAndroidViewSurface = false; - /// Returns a widget displaying the map view. - /// - /// This method includes a parameter for platforms that require a text - /// direction. For example, this should be used when using hybrid composition - /// on Android. + @override Widget buildViewWithTextDirection( int creationId, PlatformViewCreatedCallback onPlatformViewCreated, { @@ -473,79 +469,6 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { Set tileOverlays = const {}, Set>? gestureRecognizers, Map mapOptions = const {}, - }) { - if (defaultTargetPlatform == TargetPlatform.android && - useAndroidViewSurface) { - final Map creationParams = { - 'initialCameraPosition': initialCameraPosition.toMap(), - 'options': mapOptions, - 'markersToAdd': serializeMarkerSet(markers), - 'polygonsToAdd': serializePolygonSet(polygons), - 'polylinesToAdd': serializePolylineSet(polylines), - 'circlesToAdd': serializeCircleSet(circles), - 'tileOverlaysToAdd': serializeTileOverlaySet(tileOverlays), - }; - return PlatformViewLink( - viewType: 'plugins.flutter.io/google_maps', - surfaceFactory: ( - BuildContext context, - PlatformViewController controller, - ) { - return AndroidViewSurface( - controller: controller as AndroidViewController, - gestureRecognizers: gestureRecognizers ?? - const >{}, - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - ); - }, - onCreatePlatformView: (PlatformViewCreationParams params) { - final SurfaceAndroidViewController controller = - PlatformViewsService.initSurfaceAndroidView( - id: params.id, - viewType: 'plugins.flutter.io/google_maps', - layoutDirection: textDirection, - creationParams: creationParams, - creationParamsCodec: const StandardMessageCodec(), - onFocus: () => params.onFocusChanged(true), - ); - controller.addOnPlatformViewCreatedListener( - params.onPlatformViewCreated, - ); - controller.addOnPlatformViewCreatedListener( - onPlatformViewCreated, - ); - - controller.create(); - return controller; - }, - ); - } - return buildView( - creationId, - onPlatformViewCreated, - initialCameraPosition: initialCameraPosition, - markers: markers, - polygons: polygons, - polylines: polylines, - circles: circles, - tileOverlays: tileOverlays, - gestureRecognizers: gestureRecognizers, - mapOptions: mapOptions, - ); - } - - @override - Widget buildView( - int creationId, - PlatformViewCreatedCallback onPlatformViewCreated, { - required CameraPosition initialCameraPosition, - Set markers = const {}, - Set polygons = const {}, - Set polylines = const {}, - Set circles = const {}, - Set tileOverlays = const {}, - Set>? gestureRecognizers, - Map mapOptions = const {}, }) { final Map creationParams = { 'initialCameraPosition': initialCameraPosition.toMap(), @@ -556,14 +479,52 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { 'circlesToAdd': serializeCircleSet(circles), 'tileOverlaysToAdd': serializeTileOverlaySet(tileOverlays), }; + if (defaultTargetPlatform == TargetPlatform.android) { - return AndroidView( - viewType: 'plugins.flutter.io/google_maps', - onPlatformViewCreated: onPlatformViewCreated, - gestureRecognizers: gestureRecognizers, - creationParams: creationParams, - creationParamsCodec: const StandardMessageCodec(), - ); + if (useAndroidViewSurface) { + return PlatformViewLink( + viewType: 'plugins.flutter.io/google_maps', + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: gestureRecognizers ?? + const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + final SurfaceAndroidViewController controller = + PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: 'plugins.flutter.io/google_maps', + layoutDirection: textDirection, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + onFocus: () => params.onFocusChanged(true), + ); + controller.addOnPlatformViewCreatedListener( + params.onPlatformViewCreated, + ); + controller.addOnPlatformViewCreatedListener( + onPlatformViewCreated, + ); + + controller.create(); + return controller; + }, + ); + } else { + return AndroidView( + viewType: 'plugins.flutter.io/google_maps', + onPlatformViewCreated: onPlatformViewCreated, + gestureRecognizers: gestureRecognizers, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ); + } } else if (defaultTargetPlatform == TargetPlatform.iOS) { return UiKitView( viewType: 'plugins.flutter.io/google_maps', @@ -573,7 +534,36 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { creationParamsCodec: const StandardMessageCodec(), ); } + return Text( '$defaultTargetPlatform is not yet supported by the maps plugin'); } + + @override + Widget buildView( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + return buildViewWithTextDirection( + creationId, + onPlatformViewCreated, + initialCameraPosition: initialCameraPosition, + textDirection: TextDirection.ltr, + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + tileOverlays: tileOverlays, + gestureRecognizers: gestureRecognizers, + mapOptions: mapOptions, + ); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart index 425e040ee812..2bb0ab2588f9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart @@ -338,7 +338,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { throw UnimplementedError('dispose() has not been implemented.'); } - /// Returns a widget displaying the map view + /// Returns a widget displaying the map view. Widget buildView( int creationId, PlatformViewCreatedCallback onPlatformViewCreated, { @@ -356,4 +356,40 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { }) { throw UnimplementedError('buildView() has not been implemented.'); } + + /// Returns a widget displaying the map view. + /// + /// This method is similar to [buildView], but contains a parameter for + /// platforms that require a text direction. + /// + /// Default behavior passes all parameters except `textDirection` to + /// [buildView]. This is for backward compatibility with existing + /// implementations. Platforms that use the text direction should override + /// this as the primary implementation, and delegate to it from buildView. + Widget buildViewWithTextDirection( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + required TextDirection textDirection, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + return buildView( + creationId, + onPlatformViewCreated, + initialCameraPosition: initialCameraPosition, + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + tileOverlays: tileOverlays, + gestureRecognizers: gestureRecognizers, + mapOptions: mapOptions, + ); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml index 1ea425ea0273..1dc73f442d2e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.1.0 +version: 2.1.1 environment: sdk: '>=2.12.0 <3.0.0' diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart index 2c50313ab8a6..de4edf375696 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart @@ -2,6 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; import 'package:mockito/mockito.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; @@ -34,6 +38,23 @@ void main() { test('Can be extended', () { GoogleMapsFlutterPlatform.instance = ExtendsGoogleMapsFlutterPlatform(); }); + + test( + 'default implementation of `buildViewWithTextDirection` delegates to `buildView`', + () { + final GoogleMapsFlutterPlatform platform = + BuildViewGoogleMapsFlutterPlatform(); + expect( + platform.buildViewWithTextDirection( + 0, + (_) {}, + initialCameraPosition: CameraPosition(target: LatLng(0.0, 0.0)), + textDirection: TextDirection.ltr, + ), + isA(), + ); + }, + ); }); } @@ -45,3 +66,22 @@ class ImplementsGoogleMapsFlutterPlatform extends Mock implements GoogleMapsFlutterPlatform {} class ExtendsGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform {} + +class BuildViewGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { + @override + Widget buildView( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers = + const >{}, + Map mapOptions = const {}, + }) { + return const Text(''); + } +} From 256d37dd2198b71edeeb4e6fe42ec657045c1516 Mon Sep 17 00:00:00 2001 From: Marcus Date: Tue, 10 Aug 2021 00:05:07 +0200 Subject: [PATCH 058/123] [image_picker] fix camera on Android 11 (#3194) --- .../image_picker/image_picker/CHANGELOG.md | 5 ++ .../imagepicker/ImagePickerDelegate.java | 54 +++++++++---------- .../imagepicker/ImagePickerDelegateTest.java | 16 +++--- .../image_picker/image_picker/pubspec.yaml | 2 +- 4 files changed, 37 insertions(+), 40 deletions(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index f9c7640183d5..9d89389cb105 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.3+2 + +* Fix using Camera as image source on Android 11+ + ## 0.8.3+1 * Fixed README Example. @@ -26,6 +30,7 @@ * Fix image picker causing a crash when the cache directory is deleted. ## 0.8.1+2 + * Update the example app to support the multi-image feature. ## 0.8.1+1 diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index 8b904f5d769d..dbd0f70af936 100644 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -6,6 +6,7 @@ import android.Manifest; import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -88,7 +89,6 @@ public class ImagePickerDelegate private final ImageResizer imageResizer; private final ImagePickerCache cache; private final PermissionManager permissionManager; - private final IntentResolver intentResolver; private final FileUriResolver fileUriResolver; private final FileUtils fileUtils; private CameraDevice cameraDevice; @@ -101,10 +101,6 @@ interface PermissionManager { boolean needRequestCameraPermission(); } - interface IntentResolver { - boolean resolveActivity(Intent intent); - } - interface FileUriResolver { Uri resolveFileProviderUriForFile(String fileProviderName, File imageFile); @@ -148,12 +144,6 @@ public boolean needRequestCameraPermission() { return ImagePickerUtils.needRequestCameraPermission(activity); } }, - new IntentResolver() { - @Override - public boolean resolveActivity(Intent intent) { - return intent.resolveActivity(activity.getPackageManager()) != null; - } - }, new FileUriResolver() { @Override public Uri resolveFileProviderUriForFile(String fileProviderName, File file) { @@ -190,7 +180,6 @@ public void onScanCompleted(String path, Uri uri) { final MethodCall methodCall, final ImagePickerCache cache, final PermissionManager permissionManager, - final IntentResolver intentResolver, final FileUriResolver fileUriResolver, final FileUtils fileUtils) { this.activity = activity; @@ -200,7 +189,6 @@ public void onScanCompleted(String path, Uri uri) { this.pendingResult = result; this.methodCall = methodCall; this.permissionManager = permissionManager; - this.intentResolver = intentResolver; this.fileUriResolver = fileUriResolver; this.fileUtils = fileUtils; this.cache = cache; @@ -291,13 +279,6 @@ private void launchTakeVideoWithCameraIntent() { useFrontCamera(intent); } - boolean canTakePhotos = intentResolver.resolveActivity(intent); - - if (!canTakePhotos) { - finishWithError("no_available_camera", "No cameras available for taking pictures."); - return; - } - File videoFile = createTemporaryWritableVideoFile(); pendingCameraMediaUri = Uri.parse("file:" + videoFile.getAbsolutePath()); @@ -305,7 +286,18 @@ private void launchTakeVideoWithCameraIntent() { intent.putExtra(MediaStore.EXTRA_OUTPUT, videoUri); grantUriPermissions(intent, videoUri); - activity.startActivityForResult(intent, REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA); + try { + activity.startActivityForResult(intent, REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA); + } catch (ActivityNotFoundException e) { + try { + // If we can't delete the file again here, there's not really anything we can do about it. + //noinspection ResultOfMethodCallIgnored + videoFile.delete(); + } catch (SecurityException exception) { + exception.printStackTrace(); + } + finishWithError("no_available_camera", "No cameras available for taking pictures."); + } } public void chooseImageFromGallery(MethodCall methodCall, MethodChannel.Result result) { @@ -371,13 +363,6 @@ private void launchTakeImageWithCameraIntent() { useFrontCamera(intent); } - boolean canTakePhotos = intentResolver.resolveActivity(intent); - - if (!canTakePhotos) { - finishWithError("no_available_camera", "No cameras available for taking pictures."); - return; - } - File imageFile = createTemporaryWritableImageFile(); pendingCameraMediaUri = Uri.parse("file:" + imageFile.getAbsolutePath()); @@ -385,7 +370,18 @@ private void launchTakeImageWithCameraIntent() { intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); grantUriPermissions(intent, imageUri); - activity.startActivityForResult(intent, REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA); + try { + activity.startActivityForResult(intent, REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA); + } catch (ActivityNotFoundException e) { + try { + // If we can't delete the file again here, there's not really anything we can do about it. + //noinspection ResultOfMethodCallIgnored + imageFile.delete(); + } catch (SecurityException exception) { + exception.printStackTrace(); + } + finishWithError("no_available_camera", "No cameras available for taking pictures."); + } } private File createTemporaryWritableImageFile() { diff --git a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index 1b55a7569eac..ebd58d05fee4 100644 --- a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java +++ b/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -7,7 +7,9 @@ import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -16,6 +18,7 @@ import android.Manifest; import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -42,7 +45,6 @@ public class ImagePickerDelegateTest { @Mock MethodCall mockMethodCall; @Mock MethodChannel.Result mockResult; @Mock ImagePickerDelegate.PermissionManager mockPermissionManager; - @Mock ImagePickerDelegate.IntentResolver mockIntentResolver; @Mock FileUtils mockFileUtils; @Mock Intent mockIntent; @Mock ImagePickerCache cache; @@ -164,7 +166,6 @@ public void takeImageWithCamera_WhenHasNoCameraPermission_RequestsForPermission( @Test public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermission() { when(mockPermissionManager.needRequestCameraPermission()).thenReturn(false); - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); ImagePickerDelegate delegate = createDelegate(); delegate.takeImageWithCamera(mockMethodCall, mockResult); @@ -178,7 +179,6 @@ public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermis public void takeImageWithCamera_WhenHasCameraPermission_AndAnActivityCanHandleCameraIntent_LaunchesTakeWithCameraIntent() { when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true); - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); ImagePickerDelegate delegate = createDelegate(); delegate.takeImageWithCamera(mockMethodCall, mockResult); @@ -192,8 +192,9 @@ public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermis public void takeImageWithCamera_WhenHasCameraPermission_AndNoActivityToHandleCameraIntent_FinishesWithNoCamerasAvailableError() { when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true); - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(false); - + doThrow(ActivityNotFoundException.class) + .when(mockActivity) + .startActivityForResult(any(Intent.class), anyInt()); ImagePickerDelegate delegate = createDelegate(); delegate.takeImageWithCamera(mockMethodCall, mockResult); @@ -205,7 +206,6 @@ public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermis @Test public void takeImageWithCamera_WritesImageToCacheDirectory() { when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true); - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); ImagePickerDelegate delegate = createDelegate(); delegate.takeImageWithCamera(mockMethodCall, mockResult); @@ -231,7 +231,6 @@ public void onRequestPermissionsResult_WhenCameraPermissionDenied_FinishesWithEr @Test public void onRequestTakeVideoPermissionsResult_WhenCameraPermissionGranted_LaunchesTakeVideoWithCameraIntent() { - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); delegate.onRequestPermissionsResult( @@ -247,7 +246,6 @@ public void onRequestPermissionsResult_WhenCameraPermissionDenied_FinishesWithEr @Test public void onRequestTakeImagePermissionsResult_WhenCameraPermissionGranted_LaunchesTakeWithCameraIntent() { - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); delegate.onRequestPermissionsResult( @@ -379,7 +377,6 @@ private ImagePickerDelegate createDelegate() { null, cache, mockPermissionManager, - mockIntentResolver, mockFileUriResolver, mockFileUtils); } @@ -393,7 +390,6 @@ private ImagePickerDelegate createDelegateWithPendingResultAndMethodCall() { mockMethodCall, cache, mockPermissionManager, - mockIntentResolver, mockFileUriResolver, mockFileUtils); } diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index e67e79fbba14..e167d8ab891c 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.3+1 +version: 0.8.3+2 environment: sdk: ">=2.12.0 <3.0.0" From d31bd7db62cb141ce98d2bf1286a606f017b607f Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Tue, 10 Aug 2021 11:57:06 -0700 Subject: [PATCH 059/123] [webview_flutter] Only call onWebResourceError for main frame (#3078) --- .../webview_flutter/CHANGELOG.md | 4 +- .../webviewflutter/FlutterWebViewClient.java | 17 ++++--- .../webview_flutter_test.dart | 44 +++++++++++++++++++ .../webview_flutter/lib/webview_flutter.dart | 3 +- .../webview_flutter/pubspec.yaml | 2 +- 5 files changed, 61 insertions(+), 9 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index fcfaf4e5720d..df7d9cb87457 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,6 +1,8 @@ -## NEXT +## 2.0.12 * Improved the documentation on using the different Android Platform View modes. +* So that Android and iOS behave the same, `onWebResourceError` is now only called for the main + page. ## 2.0.11 diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java index 4e7056f1468c..adc84671a701 100644 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java @@ -14,6 +14,7 @@ import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; +import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.webkit.WebResourceErrorCompat; import androidx.webkit.WebViewClientCompat; @@ -192,8 +193,10 @@ public void onPageFinished(WebView view, String url) { @Override public void onReceivedError( WebView view, WebResourceRequest request, WebResourceError error) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); + if (request.isForMainFrame()) { + FlutterWebViewClient.this.onWebResourceError( + error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); + } } @Override @@ -239,9 +242,13 @@ public void onPageFinished(WebView view, String url) { @SuppressLint("RequiresFeature") @Override public void onReceivedError( - WebView view, WebResourceRequest request, WebResourceErrorCompat error) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); + @NonNull WebView view, + @NonNull WebResourceRequest request, + @NonNull WebResourceErrorCompat error) { + if (request.isForMainFrame()) { + FlutterWebViewClient.this.onWebResourceError( + error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); + } } @Override diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart index 876f961a353b..f3eeee156421 100644 --- a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -1139,6 +1139,7 @@ void main() { (WidgetTester tester) async { final Completer errorCompleter = Completer(); + final Completer pageFinishCompleter = Completer(); await tester.pumpWidget( Directionality( @@ -1150,13 +1151,56 @@ void main() { onWebResourceError: (WebResourceError error) { errorCompleter.complete(error); }, + onPageFinished: (_) => pageFinishCompleter.complete(), ), ), ); expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; }); + testWidgets( + 'onWebResourceError only called for main frame', + (WidgetTester tester) async { + final String iframeTest = ''' + + + + WebResourceError test + + + + + + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframeTest)); + + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,$iframeTestBase64', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }, + ); + testWidgets('can block requests', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); diff --git a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart index 74d8af8d4687..398ac876bf3e 100644 --- a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart @@ -369,8 +369,7 @@ class WebView extends StatefulWidget { /// Invoked when a web resource has failed to load. /// - /// This can be called for any resource (iframe, image, etc.), not just for - /// the main page. + /// This callback is only called for the main page. final WebResourceErrorCallback? onWebResourceError; /// Controls whether WebView debugging is enabled. diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 2f00071e772e..cc5d9cdc8b96 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.0.11 +version: 2.0.12 environment: sdk: ">=2.12.0 <3.0.0" From e3d5ef0ddb77c880bc37a7530cd5cb0ba7523a92 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Wed, 11 Aug 2021 01:02:05 +0200 Subject: [PATCH 060/123] [camera_web] Add support for a zoom level (#4224) --- .../camera_settings_test.dart | 153 +++++++ .../example/integration_test/camera_test.dart | 174 +++++++ .../integration_test/camera_web_test.dart | 425 +++++++++++++++++- .../camera/camera_web/lib/src/camera.dart | 50 +++ .../camera_web/lib/src/camera_settings.dart | 61 +++ .../camera/camera_web/lib/src/camera_web.dart | 35 +- .../lib/src/types/camera_error_code.dart | 8 + .../camera_web/lib/src/types/types.dart | 1 + .../lib/src/types/zoom_level_capability.dart | 45 ++ packages/camera/camera_web/pubspec.yaml | 1 + .../camera/camera_web/test/helpers/mocks.dart | 3 + .../test/types/camera_error_code_test.dart | 14 + .../types/zoom_level_capability_test.dart | 47 ++ 13 files changed, 993 insertions(+), 24 deletions(-) create mode 100644 packages/camera/camera_web/lib/src/types/zoom_level_capability.dart create mode 100644 packages/camera/camera_web/test/types/zoom_level_capability_test.dart diff --git a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart index bc228b2e35c6..0e1d78789f08 100644 --- a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart @@ -4,8 +4,10 @@ import 'dart:html'; import 'dart:ui'; +import 'dart:js_util' as js_util; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_settings.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/services.dart'; @@ -342,6 +344,157 @@ void main() { }); }); + group('getZoomLevelCapabilityForCamera', () { + late Camera camera; + late List videoTracks; + + setUp(() { + camera = MockCamera(); + videoTracks = [MockMediaStreamTrack(), MockMediaStreamTrack()]; + + when(() => camera.textureId).thenReturn(0); + when(() => camera.stream).thenReturn(FakeMediaStream(videoTracks)); + }); + + testWidgets( + 'returns the zoom level capability ' + 'based on the first video track', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'zoom': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'zoom': js_util.jsify({ + 'min': 100, + 'max': 400, + 'step': 2, + }), + }); + + final zoomLevelCapability = + settings.getZoomLevelCapabilityForCamera(camera); + + expect(zoomLevelCapability.minimum, equals(100.0)); + expect(zoomLevelCapability.maximum, equals(400.0)); + expect(zoomLevelCapability.videoTrack, equals(videoTracks.first)); + }); + + group('throws CameraWebException', () { + testWidgets( + 'with zoomLevelNotSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + expect( + () => settings.getZoomLevelCapabilityForCamera(camera), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + camera.textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with zoomLevelNotSupported error ' + 'when the zoom level is not supported ' + 'in the browser', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'zoom': false, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'zoom': { + 'min': 100, + 'max': 400, + 'step': 2, + }, + }); + + expect( + () => settings.getZoomLevelCapabilityForCamera(camera), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + camera.textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with zoomLevelNotSupported error ' + 'when the zoom level is not supported ' + 'by the camera', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'zoom': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({}); + + expect( + () => settings.getZoomLevelCapabilityForCamera(camera), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + camera.textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with notStarted error ' + 'when the camera stream has not been initialized', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'zoom': true, + }); + + // Create a camera stream with no video tracks. + when(() => camera.stream).thenReturn(FakeMediaStream([])); + + expect( + () => settings.getZoomLevelCapabilityForCamera(camera), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + camera.textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.notStarted, + ), + ), + ); + }); + }); + }); + group('getFacingModeForVideoTrack', () { testWidgets( 'throws PlatformException ' diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 740e24f87819..03ffe81cad64 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -627,6 +627,180 @@ void main() { }); }); + group('zoomLevel', () { + group('getMaxZoomLevel', () { + testWidgets( + 'returns maximum ' + 'from CameraSettings.getZoomLevelCapabilityForCamera', + (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ); + + final zoomLevelCapability = ZoomLevelCapability( + minimum: 50.0, + maximum: 100.0, + videoTrack: MockMediaStreamTrack(), + ); + + when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + .thenReturn(zoomLevelCapability); + + final maximumZoomLevel = camera.getMaxZoomLevel(); + + verify(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + .called(1); + + expect( + maximumZoomLevel, + equals(zoomLevelCapability.maximum), + ); + }); + }); + + group('getMinZoomLevel', () { + testWidgets( + 'returns minimum ' + 'from CameraSettings.getZoomLevelCapabilityForCamera', + (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ); + + final zoomLevelCapability = ZoomLevelCapability( + minimum: 50.0, + maximum: 100.0, + videoTrack: MockMediaStreamTrack(), + ); + + when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + .thenReturn(zoomLevelCapability); + + final minimumZoomLevel = camera.getMinZoomLevel(); + + verify(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + .called(1); + + expect( + minimumZoomLevel, + equals(zoomLevelCapability.minimum), + ); + }); + }); + + group('setZoomLevel', () { + testWidgets( + 'applies zoom on the video track ' + 'from CameraSettings.getZoomLevelCapabilityForCamera', + (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ); + + final videoTrack = MockMediaStreamTrack(); + + final zoomLevelCapability = ZoomLevelCapability( + minimum: 50.0, + maximum: 100.0, + videoTrack: videoTrack, + ); + + when(() => videoTrack.applyConstraints(any())) + .thenAnswer((_) async {}); + + when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + .thenReturn(zoomLevelCapability); + + const zoom = 75.0; + + camera.setZoomLevel(zoom); + + verify( + () => videoTrack.applyConstraints({ + "advanced": [ + { + ZoomLevelCapability.constraintName: zoom, + } + ] + }), + ).called(1); + }); + + group('throws CameraWebException', () { + testWidgets( + 'with zoomLevelInvalid error ' + 'when the provided zoom level is below minimum', (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ); + + final zoomLevelCapability = ZoomLevelCapability( + minimum: 50.0, + maximum: 100.0, + videoTrack: MockMediaStreamTrack(), + ); + + when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + .thenReturn(zoomLevelCapability); + + expect( + () => camera.setZoomLevel(45.0), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelInvalid, + ), + )); + }); + + testWidgets( + 'with zoomLevelInvalid error ' + 'when the provided zoom level is below minimum', (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ); + + final zoomLevelCapability = ZoomLevelCapability( + minimum: 50.0, + maximum: 100.0, + videoTrack: MockMediaStreamTrack(), + ); + + when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + .thenReturn(zoomLevelCapability); + + expect( + () => camera.setZoomLevel(105.0), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelInvalid, + ), + )); + }); + }); + }); + }); + group('getViewType', () { testWidgets('returns a correct view type', (tester) async { final camera = Camera( diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index fda35dd088c1..eb988f49ab87 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -1131,28 +1131,302 @@ void main() { ); }); - testWidgets('getMaxZoomLevel throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.getMaxZoomLevel(cameraId), - throwsUnimplementedError, - ); + group('getMaxZoomLevel', () { + testWidgets('calls getMaxZoomLevel on the camera', (tester) async { + final camera = MockCamera(); + const maximumZoomLevel = 100.0; + + when(camera.getMaxZoomLevel).thenReturn(maximumZoomLevel); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + await CameraPlatform.instance.getMaxZoomLevel( + cameraId, + ), + equals(maximumZoomLevel), + ); + + verify(camera.getMaxZoomLevel).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () async => await CameraPlatform.instance.getMaxZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when getMaxZoomLevel throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(camera.getMaxZoomLevel).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.getMaxZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when getMaxZoomLevel throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.getMaxZoomLevel).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.getMaxZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); }); - testWidgets('getMinZoomLevel throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.getMinZoomLevel(cameraId), - throwsUnimplementedError, - ); + group('getMinZoomLevel', () { + testWidgets('calls getMinZoomLevel on the camera', (tester) async { + final camera = MockCamera(); + const minimumZoomLevel = 100.0; + + when(camera.getMinZoomLevel).thenReturn(minimumZoomLevel); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + await CameraPlatform.instance.getMinZoomLevel( + cameraId, + ), + equals(minimumZoomLevel), + ); + + verify(camera.getMinZoomLevel).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () async => await CameraPlatform.instance.getMinZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when getMinZoomLevel throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(camera.getMinZoomLevel).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.getMinZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when getMinZoomLevel throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.getMinZoomLevel).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.getMinZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); }); - testWidgets('setZoomLevel throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.setZoomLevel( - cameraId, - 1.0, - ), - throwsUnimplementedError, - ); + group('setZoomLevel', () { + testWidgets('calls setZoomLevel on the camera', (tester) async { + final camera = MockCamera(); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + const zoom = 100.0; + + await CameraPlatform.instance.setZoomLevel(cameraId, zoom); + + verify(() => camera.setZoomLevel(zoom)).called(1); + }); + + group('throws CameraException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () async => await CameraPlatform.instance.setZoomLevel( + cameraId, + 100.0, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when setZoomLevel throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(() => camera.setZoomLevel(any())).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.setZoomLevel( + cameraId, + 100.0, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when setZoomLevel throws PlatformException', + (tester) async { + final camera = MockCamera(); + final exception = PlatformException( + code: CameraErrorCode.notSupported.toString(), + message: 'message', + ); + + when(() => camera.setZoomLevel(any())).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.setZoomLevel( + cameraId, + 100.0, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code, + ), + ), + ); + }); + + testWidgets('when setZoomLevel throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(() => camera.setZoomLevel(any())).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.setZoomLevel( + cameraId, + 100.0, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); }); testWidgets( @@ -1538,6 +1812,121 @@ void main() { await streamQueue.cancel(); }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on getMaxZoomLevel error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.zoomLevelNotSupported, + 'description', + ); + + when(camera.getMaxZoomLevel).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.getMaxZoomLevel( + cameraId, + ), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on getMinZoomLevel error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.zoomLevelNotSupported, + 'description', + ); + + when(camera.getMinZoomLevel).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.getMinZoomLevel( + cameraId, + ), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on setZoomLevel error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.zoomLevelNotSupported, + 'description', + ); + + when(() => camera.setZoomLevel(any())).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.setZoomLevel( + cameraId, + 100.0, + ), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); }); testWidgets('onVideoRecordedEvent throws UnimplementedError', diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 1cd007b917bb..c77d36023058 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -28,6 +28,10 @@ String _getViewType(int cameraId) => 'plugins.flutter.io/camera_$cameraId'; /// The camera can be played/stopped by calling [play]/[stop] /// or may capture a picture by calling [takePicture]. /// +/// The camera zoom may be adjusted with [setZoomLevel]. The provided +/// zoom level must be a value in the range of [getMinZoomLevel] to +/// [getMaxZoomLevel]. +/// /// The [textureId] is used to register a camera view with the id /// defined by [_getViewType]. class Camera { @@ -182,6 +186,9 @@ class Camera { } /// Sets the camera flash mode to [mode]. + /// + /// Throws a [CameraWebException] if the torch mode is not supported + /// or the camera has not been initialized or started. void setFlashMode(FlashMode mode) { final mediaDevices = window?.navigator.mediaDevices; final supportedConstraints = mediaDevices?.getSupportedConstraints(); @@ -203,6 +210,9 @@ class Camera { } /// Sets the camera torch mode constraint to [enabled]. + /// + /// Throws a [CameraWebException] if the torch mode is not supported + /// or the camera has not been initialized or started. void _setTorchMode({required bool enabled}) { final videoTracks = stream?.getVideoTracks() ?? []; @@ -236,6 +246,46 @@ class Camera { } } + /// Returns the camera maximum zoom level. + /// + /// Throws a [CameraWebException] if the zoom level is not supported + /// or the camera has not been initialized or started. + double getMaxZoomLevel() => + _cameraSettings.getZoomLevelCapabilityForCamera(this).maximum; + + /// Returns the camera minimum zoom level. + /// + /// Throws a [CameraWebException] if the zoom level is not supported + /// or the camera has not been initialized or started. + double getMinZoomLevel() => + _cameraSettings.getZoomLevelCapabilityForCamera(this).minimum; + + /// Sets the camera zoom level to [zoom]. + /// + /// Throws a [CameraWebException] if the zoom level is invalid, + /// not supported or the camera has not been initialized or started. + void setZoomLevel(double zoom) { + final zoomLevelCapability = + _cameraSettings.getZoomLevelCapabilityForCamera(this); + + if (zoom < zoomLevelCapability.minimum || + zoom > zoomLevelCapability.maximum) { + throw CameraWebException( + textureId, + CameraErrorCode.zoomLevelInvalid, + 'The provided zoom level must be in the range of ${zoomLevelCapability.minimum} to ${zoomLevelCapability.maximum}.', + ); + } + + zoomLevelCapability.videoTrack.applyConstraints({ + "advanced": [ + { + ZoomLevelCapability.constraintName: zoom, + } + ] + }); + } + /// Returns the registered view type of the camera. String getViewType() => _getViewType(textureId); diff --git a/packages/camera/camera_web/lib/src/camera_settings.dart b/packages/camera/camera_web/lib/src/camera_settings.dart index ce713bc52468..7d35fff84112 100644 --- a/packages/camera/camera_web/lib/src/camera_settings.dart +++ b/packages/camera/camera_web/lib/src/camera_settings.dart @@ -4,8 +4,10 @@ import 'dart:html' as html; import 'dart:ui'; +import 'dart:js_util' as js_util; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -107,6 +109,65 @@ class CameraSettings { } } + /// Returns the zoom level capability for the given [camera]. + /// + /// Throws a [CameraWebException] if the zoom level is not supported + /// or the camera has not been initialized or started. + ZoomLevelCapability getZoomLevelCapabilityForCamera( + Camera camera, + ) { + final mediaDevices = window?.navigator.mediaDevices; + final supportedConstraints = mediaDevices?.getSupportedConstraints(); + final zoomLevelSupported = + supportedConstraints?[ZoomLevelCapability.constraintName] ?? false; + + if (!zoomLevelSupported) { + throw CameraWebException( + camera.textureId, + CameraErrorCode.zoomLevelNotSupported, + 'The zoom level is not supported in the current browser.', + ); + } + + final videoTracks = camera.stream?.getVideoTracks() ?? []; + + if (videoTracks.isNotEmpty) { + final defaultVideoTrack = videoTracks.first; + + /// The zoom level capability is represented by MediaSettingsRange. + /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaSettingsRange + final zoomLevelCapability = defaultVideoTrack + .getCapabilities()[ZoomLevelCapability.constraintName] ?? + {}; + + // The zoom level capability is a nested JS object, therefore + // we need to access its properties with the js_util library. + // See: https://api.dart.dev/stable/2.13.4/dart-js_util/getProperty.html + final minimumZoomLevel = js_util.getProperty(zoomLevelCapability, 'min'); + final maximumZoomLevel = js_util.getProperty(zoomLevelCapability, 'max'); + + if (minimumZoomLevel != null && maximumZoomLevel != null) { + return ZoomLevelCapability( + minimum: minimumZoomLevel.toDouble(), + maximum: maximumZoomLevel.toDouble(), + videoTrack: defaultVideoTrack, + ); + } else { + throw CameraWebException( + camera.textureId, + CameraErrorCode.zoomLevelNotSupported, + 'The zoom level is not supported by the current camera.', + ); + } + } else { + throw CameraWebException( + camera.textureId, + CameraErrorCode.notStarted, + 'The camera has not been initialized or started.', + ); + } + } + /// Returns a facing mode of the [videoTrack] /// (null if the facing mode is not available). String? getFacingModeForVideoTrack(html.MediaStreamTrack videoTrack) { diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 0ae0d9e75c24..ecea3a76e74a 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -481,18 +481,41 @@ class CameraPlugin extends CameraPlatform { } @override - Future getMaxZoomLevel(int cameraId) { - throw UnimplementedError('getMaxZoomLevel() is not implemented.'); + Future getMaxZoomLevel(int cameraId) async { + try { + return getCamera(cameraId).getMaxZoomLevel(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } } @override - Future getMinZoomLevel(int cameraId) { - throw UnimplementedError('getMinZoomLevel() is not implemented.'); + Future getMinZoomLevel(int cameraId) async { + try { + return getCamera(cameraId).getMinZoomLevel(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } } @override - Future setZoomLevel(int cameraId, double zoom) { - throw UnimplementedError('setZoomLevel() is not implemented.'); + Future setZoomLevel(int cameraId, double zoom) async { + try { + getCamera(cameraId).setZoomLevel(zoom); + } on html.DomException catch (e) { + throw CameraException(e.name, e.message); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw CameraException(e.code.toString(), e.description); + } } @override diff --git a/packages/camera/camera_web/lib/src/types/camera_error_code.dart b/packages/camera/camera_web/lib/src/types/camera_error_code.dart index 904920db6ac6..210fa2baa9d2 100644 --- a/packages/camera/camera_web/lib/src/types/camera_error_code.dart +++ b/packages/camera/camera_web/lib/src/types/camera_error_code.dart @@ -56,6 +56,14 @@ class CameraErrorCode { static const CameraErrorCode torchModeNotSupported = CameraErrorCode._('torchModeNotSupported'); + /// The camera zoom level is not supported. + static const CameraErrorCode zoomLevelNotSupported = + CameraErrorCode._('zoomLevelNotSupported'); + + /// The camera zoom level is invalid. + static const CameraErrorCode zoomLevelInvalid = + CameraErrorCode._('zoomLevelInvalid'); + /// The camera has not been initialized or started. static const CameraErrorCode notStarted = CameraErrorCode._('cameraNotStarted'); diff --git a/packages/camera/camera_web/lib/src/types/types.dart b/packages/camera/camera_web/lib/src/types/types.dart index 4e3902fcb3ee..72d7fb85af14 100644 --- a/packages/camera/camera_web/lib/src/types/types.dart +++ b/packages/camera/camera_web/lib/src/types/types.dart @@ -7,3 +7,4 @@ export 'camera_options.dart'; export 'camera_web_exception.dart'; export 'media_device_kind.dart'; export 'orientation_type.dart'; +export 'zoom_level_capability.dart'; diff --git a/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart b/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart new file mode 100644 index 000000000000..ace57140d956 --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart @@ -0,0 +1,45 @@ +// 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 'dart:html' as html; +import 'dart:ui' show hashValues; + +/// The possible range of values for the zoom level configurable +/// on the camera video track. +class ZoomLevelCapability { + /// Creates a new instance of [ZoomLevelCapability] with the given + /// zoom level range of [minimum] to [maximum] configurable + /// on the [videoTrack]. + ZoomLevelCapability({ + required this.minimum, + required this.maximum, + required this.videoTrack, + }); + + /// The zoom level constraint name. + /// See: https://w3c.github.io/mediacapture-image/#dom-mediatracksupportedconstraints-zoom + static const constraintName = "zoom"; + + /// The minimum zoom level. + final double minimum; + + /// The maximum zoom level. + final double maximum; + + /// The video track capable of configuring the zoom level. + final html.MediaStreamTrack videoTrack; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is ZoomLevelCapability && + other.minimum == minimum && + other.maximum == maximum && + other.videoTrack == videoTrack; + } + + @override + int get hashCode => hashValues(minimum, maximum, videoTrack); +} diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index a2aa43c22d65..ec674f375164 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -30,4 +30,5 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + mocktail: ^0.1.4 pedantic: ^1.11.1 \ No newline at end of file diff --git a/packages/camera/camera_web/test/helpers/mocks.dart b/packages/camera/camera_web/test/helpers/mocks.dart index 0398ad33f126..34c56632b60f 100644 --- a/packages/camera/camera_web/test/helpers/mocks.dart +++ b/packages/camera/camera_web/test/helpers/mocks.dart @@ -5,6 +5,9 @@ import 'dart:html'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockMediaStreamTrack extends Mock implements MediaStreamTrack {} /// A fake [MediaError] that returns the provided error [_code]. class FakeMediaError extends Fake implements MediaError { diff --git a/packages/camera/camera_web/test/types/camera_error_code_test.dart b/packages/camera/camera_web/test/types/camera_error_code_test.dart index 1fec82b16f8d..c31dc6a9ffb0 100644 --- a/packages/camera/camera_web/test/types/camera_error_code_test.dart +++ b/packages/camera/camera_web/test/types/camera_error_code_test.dart @@ -89,6 +89,20 @@ void main() { ); }); + test('zoomLevelNotSupported', () { + expect( + CameraErrorCode.zoomLevelNotSupported.toString(), + equals('zoomLevelNotSupported'), + ); + }); + + test('zoomLevelInvalid', () { + expect( + CameraErrorCode.zoomLevelInvalid.toString(), + equals('zoomLevelInvalid'), + ); + }); + test('notStarted', () { expect( CameraErrorCode.notStarted.toString(), diff --git a/packages/camera/camera_web/test/types/zoom_level_capability_test.dart b/packages/camera/camera_web/test/types/zoom_level_capability_test.dart new file mode 100644 index 000000000000..c382b4b76cc4 --- /dev/null +++ b/packages/camera/camera_web/test/types/zoom_level_capability_test.dart @@ -0,0 +1,47 @@ +// 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:camera_web/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../helpers/helpers.dart'; + +void main() { + group('ZoomLevelCapability', () { + test('sets all properties', () { + const minimum = 100.0; + const maximum = 400.0; + final videoTrack = MockMediaStreamTrack(); + + final capability = ZoomLevelCapability( + minimum: minimum, + maximum: maximum, + videoTrack: videoTrack, + ); + + expect(capability.minimum, equals(minimum)); + expect(capability.maximum, equals(maximum)); + expect(capability.videoTrack, equals(videoTrack)); + }); + + test('supports value equality', () { + final videoTrack = MockMediaStreamTrack(); + + expect( + ZoomLevelCapability( + minimum: 0.0, + maximum: 100.0, + videoTrack: videoTrack, + ), + equals( + ZoomLevelCapability( + minimum: 0.0, + maximum: 100.0, + videoTrack: videoTrack, + ), + ), + ); + }); + }); +} From b9ac641ee37c5aa0c0d03dab88e3c302af4d1350 Mon Sep 17 00:00:00 2001 From: Casey Hillers Date: Wed, 11 Aug 2021 10:57:06 -0700 Subject: [PATCH 061/123] [ci.yaml] Auto-generate LUCI configs (#4223) --- .ci.yaml | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index 92bfc040eecb..c2b7deebab14 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -8,9 +8,39 @@ enabled_branches: - master +platform_properties: + windows: + properties: + caches: >- + [ + {"name": "vsbuild", "path": "vsbuild"}, + {"name": "pub_cache", "path": ".pub-cache"} + ] + dependencies: > + [ + {"dependency": "certs"} + ] + device_type: none + os: Windows + targets: - - name: Windows Plugins - builder: Windows Plugins - postsubmit: false + - name: Windows Plugins master channel + recipe: plugins/plugins + timeout: 30 + properties: + dependencies: > + [ + {"dependency": "vs_build"} + ] scheduler: luci + - name: Windows Plugins stable channel + recipe: plugins/plugins + timeout: 30 + properties: + channel: stable + dependencies: > + [ + {"dependency": "vs_build"} + ] + scheduler: luci From 77029678d2f2ae462f04de0ad96d361e516fd453 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 12 Aug 2021 00:32:06 +0200 Subject: [PATCH 062/123] [camera_web] Rename `CameraSettings` to `CameraService` (#4225) --- .../camera_error_code_test.dart | 45 +++---- .../camera_metadata_test.dart | 5 +- .../camera_options_test.dart | 39 +++--- ...ngs_test.dart => camera_service_test.dart} | 115 +++++++++--------- .../example/integration_test/camera_test.dart | 95 +++++++-------- .../camera_web_exception_test.dart | 7 +- .../integration_test/camera_web_test.dart | 85 +++++++------ .../integration_test/helpers/mocks.dart | 4 +- .../zoom_level_capability_test.dart | 9 +- .../camera/camera_web/lib/src/camera.dart | 22 ++-- ...mera_settings.dart => camera_service.dart} | 4 +- .../camera/camera_web/lib/src/camera_web.dart | 31 +++-- packages/camera/camera_web/pubspec.yaml | 1 - .../camera_web/test/helpers/helpers.dart | 5 - .../camera/camera_web/test/helpers/mocks.dart | 20 --- 15 files changed, 239 insertions(+), 248 deletions(-) rename packages/camera/camera_web/{test/types => example/integration_test}/camera_error_code_test.dart (73%) rename packages/camera/camera_web/{test/types => example/integration_test}/camera_metadata_test.dart (76%) rename packages/camera/camera_web/{test/types => example/integration_test}/camera_options_test.dart (82%) rename packages/camera/camera_web/example/integration_test/{camera_settings_test.dart => camera_service_test.dart} (86%) rename packages/camera/camera_web/{test/types => example/integration_test}/camera_web_exception_test.dart (80%) rename packages/camera/camera_web/{test/types => example/integration_test}/zoom_level_capability_test.dart (80%) rename packages/camera/camera_web/lib/src/{camera_settings.dart => camera_service.dart} (99%) delete mode 100644 packages/camera/camera_web/test/helpers/helpers.dart delete mode 100644 packages/camera/camera_web/test/helpers/mocks.dart diff --git a/packages/camera/camera_web/test/types/camera_error_code_test.dart b/packages/camera/camera_web/example/integration_test/camera_error_code_test.dart similarity index 73% rename from packages/camera/camera_web/test/types/camera_error_code_test.dart rename to packages/camera/camera_web/example/integration_test/camera_error_code_test.dart index c31dc6a9ffb0..d0250c6e4e26 100644 --- a/packages/camera/camera_web/test/types/camera_error_code_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_error_code_test.dart @@ -6,111 +6,114 @@ import 'dart:html'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; -import '../helpers/helpers.dart'; +import 'helpers/helpers.dart'; void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + group('CameraErrorCode', () { group('toString returns a correct type for', () { - test('notSupported', () { + testWidgets('notSupported', (tester) async { expect( CameraErrorCode.notSupported.toString(), equals('cameraNotSupported'), ); }); - test('notFound', () { + testWidgets('notFound', (tester) async { expect( CameraErrorCode.notFound.toString(), equals('cameraNotFound'), ); }); - test('notReadable', () { + testWidgets('notReadable', (tester) async { expect( CameraErrorCode.notReadable.toString(), equals('cameraNotReadable'), ); }); - test('overconstrained', () { + testWidgets('overconstrained', (tester) async { expect( CameraErrorCode.overconstrained.toString(), equals('cameraOverconstrained'), ); }); - test('permissionDenied', () { + testWidgets('permissionDenied', (tester) async { expect( CameraErrorCode.permissionDenied.toString(), equals('cameraPermission'), ); }); - test('type', () { + testWidgets('type', (tester) async { expect( CameraErrorCode.type.toString(), equals('cameraType'), ); }); - test('abort', () { + testWidgets('abort', (tester) async { expect( CameraErrorCode.abort.toString(), equals('cameraAbort'), ); }); - test('security', () { + testWidgets('security', (tester) async { expect( CameraErrorCode.security.toString(), equals('cameraSecurity'), ); }); - test('missingMetadata', () { + testWidgets('missingMetadata', (tester) async { expect( CameraErrorCode.missingMetadata.toString(), equals('cameraMissingMetadata'), ); }); - test('orientationNotSupported', () { + testWidgets('orientationNotSupported', (tester) async { expect( CameraErrorCode.orientationNotSupported.toString(), equals('orientationNotSupported'), ); }); - test('torchModeNotSupported', () { + testWidgets('torchModeNotSupported', (tester) async { expect( CameraErrorCode.torchModeNotSupported.toString(), equals('torchModeNotSupported'), ); }); - test('zoomLevelNotSupported', () { + testWidgets('zoomLevelNotSupported', (tester) async { expect( CameraErrorCode.zoomLevelNotSupported.toString(), equals('zoomLevelNotSupported'), ); }); - test('zoomLevelInvalid', () { + testWidgets('zoomLevelInvalid', (tester) async { expect( CameraErrorCode.zoomLevelInvalid.toString(), equals('zoomLevelInvalid'), ); }); - test('notStarted', () { + testWidgets('notStarted', (tester) async { expect( CameraErrorCode.notStarted.toString(), equals('cameraNotStarted'), ); }); - test('unknown', () { + testWidgets('unknown', (tester) async { expect( CameraErrorCode.unknown.toString(), equals('cameraUnknown'), @@ -118,7 +121,7 @@ void main() { }); group('fromMediaError', () { - test('with aborted error code', () { + testWidgets('with aborted error code', (tester) async { expect( CameraErrorCode.fromMediaError( FakeMediaError(MediaError.MEDIA_ERR_ABORTED), @@ -127,7 +130,7 @@ void main() { ); }); - test('with network error code', () { + testWidgets('with network error code', (tester) async { expect( CameraErrorCode.fromMediaError( FakeMediaError(MediaError.MEDIA_ERR_NETWORK), @@ -136,7 +139,7 @@ void main() { ); }); - test('with decode error code', () { + testWidgets('with decode error code', (tester) async { expect( CameraErrorCode.fromMediaError( FakeMediaError(MediaError.MEDIA_ERR_DECODE), @@ -145,7 +148,7 @@ void main() { ); }); - test('with source not supported error code', () { + testWidgets('with source not supported error code', (tester) async { expect( CameraErrorCode.fromMediaError( FakeMediaError(MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED), @@ -154,7 +157,7 @@ void main() { ); }); - test('with unknown error code', () { + testWidgets('with unknown error code', (tester) async { expect( CameraErrorCode.fromMediaError( FakeMediaError(5), diff --git a/packages/camera/camera_web/test/types/camera_metadata_test.dart b/packages/camera/camera_web/example/integration_test/camera_metadata_test.dart similarity index 76% rename from packages/camera/camera_web/test/types/camera_metadata_test.dart rename to packages/camera/camera_web/example/integration_test/camera_metadata_test.dart index c76688f768d7..36ecb3e47f31 100644 --- a/packages/camera/camera_web/test/types/camera_metadata_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_metadata_test.dart @@ -4,10 +4,13 @@ import 'package:camera_web/src/types/types.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + group('CameraMetadata', () { - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { expect( CameraMetadata( deviceId: 'deviceId', diff --git a/packages/camera/camera_web/test/types/camera_options_test.dart b/packages/camera/camera_web/example/integration_test/camera_options_test.dart similarity index 82% rename from packages/camera/camera_web/test/types/camera_options_test.dart rename to packages/camera/camera_web/example/integration_test/camera_options_test.dart index 6f60bfd5aeda..a74ba3088394 100644 --- a/packages/camera/camera_web/test/types/camera_options_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_options_test.dart @@ -4,10 +4,13 @@ import 'package:camera_web/src/types/types.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + group('CameraOptions', () { - test('serializes correctly', () { + testWidgets('serializes correctly', (tester) async { final cameraOptions = CameraOptions( audio: AudioConstraints(enabled: true), video: VideoConstraints( @@ -24,7 +27,7 @@ void main() { ); }); - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { expect( CameraOptions( audio: AudioConstraints(enabled: false), @@ -51,14 +54,14 @@ void main() { }); group('AudioConstraints', () { - test('serializes correctly', () { + testWidgets('serializes correctly', (tester) async { expect( AudioConstraints(enabled: true).toJson(), equals(true), ); }); - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { expect( AudioConstraints(enabled: true), equals(AudioConstraints(enabled: true)), @@ -67,7 +70,7 @@ void main() { }); group('VideoConstraints', () { - test('serializes correctly', () { + testWidgets('serializes correctly', (tester) async { final videoConstraints = VideoConstraints( facingMode: FacingModeConstraint.exact(CameraType.user), width: VideoSizeConstraint(ideal: 100, maximum: 100), @@ -88,7 +91,7 @@ void main() { ); }); - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { expect( VideoConstraints( facingMode: FacingModeConstraint.exact(CameraType.environment), @@ -110,25 +113,25 @@ void main() { group('FacingModeConstraint', () { group('ideal', () { - test( + testWidgets( 'serializes correctly ' - 'for environment camera type', () { + 'for environment camera type', (tester) async { expect( FacingModeConstraint(CameraType.environment).toJson(), equals({'ideal': 'environment'}), ); }); - test( + testWidgets( 'serializes correctly ' - 'for user camera type', () { + 'for user camera type', (tester) async { expect( FacingModeConstraint(CameraType.user).toJson(), equals({'ideal': 'user'}), ); }); - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { expect( FacingModeConstraint(CameraType.user), equals(FacingModeConstraint(CameraType.user)), @@ -137,25 +140,25 @@ void main() { }); group('exact', () { - test( + testWidgets( 'serializes correctly ' - 'for environment camera type', () { + 'for environment camera type', (tester) async { expect( FacingModeConstraint.exact(CameraType.environment).toJson(), equals({'exact': 'environment'}), ); }); - test( + testWidgets( 'serializes correctly ' - 'for user camera type', () { + 'for user camera type', (tester) async { expect( FacingModeConstraint.exact(CameraType.user).toJson(), equals({'exact': 'user'}), ); }); - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { expect( FacingModeConstraint.exact(CameraType.environment), equals(FacingModeConstraint.exact(CameraType.environment)), @@ -165,7 +168,7 @@ void main() { }); group('VideoSizeConstraint ', () { - test('serializes correctly', () { + testWidgets('serializes correctly', (tester) async { expect( VideoSizeConstraint( minimum: 200, @@ -180,7 +183,7 @@ void main() { ); }); - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { expect( VideoSizeConstraint( minimum: 100, diff --git a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart b/packages/camera/camera_web/example/integration_test/camera_service_test.dart similarity index 86% rename from packages/camera/camera_web/example/integration_test/camera_settings_test.dart rename to packages/camera/camera_web/example/integration_test/camera_service_test.dart index 0e1d78789f08..161aeb4a595e 100644 --- a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_service_test.dart @@ -8,7 +8,7 @@ import 'dart:js_util' as js_util; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; -import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/camera_service.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -20,13 +20,13 @@ import 'helpers/helpers.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('CameraSettings', () { + group('CameraService', () { const cameraId = 0; late Window window; late Navigator navigator; late MediaDevices mediaDevices; - late CameraSettings settings; + late CameraService cameraService; setUp(() async { window = MockWindow(); @@ -36,7 +36,7 @@ void main() { when(() => window.navigator).thenReturn(navigator); when(() => navigator.mediaDevices).thenReturn(mediaDevices); - settings = CameraSettings()..window = window; + cameraService = CameraService()..window = window; }); group('getMediaStreamForOptions', () { @@ -53,7 +53,7 @@ void main() { ), ); - await settings.getMediaStreamForOptions(options); + await cameraService.getMediaStreamForOptions(options); verify( () => mediaDevices.getUserMedia(options.toJson()), @@ -67,7 +67,7 @@ void main() { when(() => navigator.mediaDevices).thenReturn(null); expect( - () => settings.getMediaStreamForOptions(CameraOptions()), + () => cameraService.getMediaStreamForOptions(CameraOptions()), throwsA( isA().having( (e) => e.code, @@ -87,7 +87,7 @@ void main() { .thenThrow(FakeDomException('NotFoundError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -107,7 +107,7 @@ void main() { .thenThrow(FakeDomException('DevicesNotFoundError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -127,7 +127,7 @@ void main() { .thenThrow(FakeDomException('NotReadableError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -147,7 +147,7 @@ void main() { .thenThrow(FakeDomException('TrackStartError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -167,7 +167,7 @@ void main() { .thenThrow(FakeDomException('OverconstrainedError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -188,7 +188,7 @@ void main() { .thenThrow(FakeDomException('ConstraintNotSatisfiedError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -209,7 +209,7 @@ void main() { .thenThrow(FakeDomException('NotAllowedError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -230,7 +230,7 @@ void main() { .thenThrow(FakeDomException('PermissionDeniedError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -251,7 +251,7 @@ void main() { .thenThrow(FakeDomException('TypeError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -271,7 +271,7 @@ void main() { .thenThrow(FakeDomException('AbortError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -291,7 +291,7 @@ void main() { .thenThrow(FakeDomException('SecurityError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -311,7 +311,7 @@ void main() { .thenThrow(FakeDomException('Unknown')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -330,7 +330,7 @@ void main() { when(() => mediaDevices.getUserMedia(any())).thenThrow(Exception()); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -372,7 +372,7 @@ void main() { }); final zoomLevelCapability = - settings.getZoomLevelCapabilityForCamera(camera); + cameraService.getZoomLevelCapabilityForCamera(camera); expect(zoomLevelCapability.minimum, equals(100.0)); expect(zoomLevelCapability.maximum, equals(400.0)); @@ -386,7 +386,7 @@ void main() { when(() => navigator.mediaDevices).thenReturn(null); expect( - () => settings.getZoomLevelCapabilityForCamera(camera), + () => cameraService.getZoomLevelCapabilityForCamera(camera), throwsA( isA() .having( @@ -420,7 +420,7 @@ void main() { }); expect( - () => settings.getZoomLevelCapabilityForCamera(camera), + () => cameraService.getZoomLevelCapabilityForCamera(camera), throwsA( isA() .having( @@ -448,7 +448,7 @@ void main() { when(videoTracks.first.getCapabilities).thenReturn({}); expect( - () => settings.getZoomLevelCapabilityForCamera(camera), + () => cameraService.getZoomLevelCapabilityForCamera(camera), throwsA( isA() .having( @@ -476,7 +476,7 @@ void main() { when(() => camera.stream).thenReturn(FakeMediaStream([])); expect( - () => settings.getZoomLevelCapabilityForCamera(camera), + () => cameraService.getZoomLevelCapabilityForCamera(camera), throwsA( isA() .having( @@ -503,7 +503,8 @@ void main() { when(() => navigator.mediaDevices).thenReturn(null); expect( - () => settings.getFacingModeForVideoTrack(MockMediaStreamTrack()), + () => + cameraService.getFacingModeForVideoTrack(MockMediaStreamTrack()), throwsA( isA().having( (e) => e.code, @@ -522,7 +523,7 @@ void main() { }); final facingMode = - settings.getFacingModeForVideoTrack(MockMediaStreamTrack()); + cameraService.getFacingModeForVideoTrack(MockMediaStreamTrack()); expect( facingMode, @@ -544,7 +545,8 @@ void main() { when(videoTrack.getSettings).thenReturn({'facingMode': 'user'}); - final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + final facingMode = + cameraService.getFacingModeForVideoTrack(videoTrack); expect( facingMode, @@ -563,7 +565,8 @@ void main() { 'facingMode': ['environment', 'left'] }); - final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + final facingMode = + cameraService.getFacingModeForVideoTrack(videoTrack); expect( facingMode, @@ -580,7 +583,8 @@ void main() { when(videoTrack.getSettings).thenReturn({}); when(videoTrack.getCapabilities).thenReturn({'facingMode': []}); - final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + final facingMode = + cameraService.getFacingModeForVideoTrack(videoTrack); expect( facingMode, @@ -597,7 +601,8 @@ void main() { when(videoTrack.getSettings).thenReturn({}); when(videoTrack.getCapabilities).thenThrow(JSNoSuchMethodError()); - final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + final facingMode = + cameraService.getFacingModeForVideoTrack(videoTrack); expect( facingMode, @@ -616,7 +621,7 @@ void main() { when(videoTrack.getCapabilities).thenThrow(Exception('Unknown')); expect( - () => settings.getFacingModeForVideoTrack(videoTrack), + () => cameraService.getFacingModeForVideoTrack(videoTrack), throwsA( isA().having( (e) => e.code, @@ -634,7 +639,7 @@ void main() { 'returns front ' 'when the facing mode is user', (tester) async { expect( - settings.mapFacingModeToLensDirection('user'), + cameraService.mapFacingModeToLensDirection('user'), equals(CameraLensDirection.front), ); }); @@ -643,7 +648,7 @@ void main() { 'returns back ' 'when the facing mode is environment', (tester) async { expect( - settings.mapFacingModeToLensDirection('environment'), + cameraService.mapFacingModeToLensDirection('environment'), equals(CameraLensDirection.back), ); }); @@ -652,7 +657,7 @@ void main() { 'returns external ' 'when the facing mode is left', (tester) async { expect( - settings.mapFacingModeToLensDirection('left'), + cameraService.mapFacingModeToLensDirection('left'), equals(CameraLensDirection.external), ); }); @@ -661,7 +666,7 @@ void main() { 'returns external ' 'when the facing mode is right', (tester) async { expect( - settings.mapFacingModeToLensDirection('right'), + cameraService.mapFacingModeToLensDirection('right'), equals(CameraLensDirection.external), ); }); @@ -672,7 +677,7 @@ void main() { 'returns user ' 'when the facing mode is user', (tester) async { expect( - settings.mapFacingModeToCameraType('user'), + cameraService.mapFacingModeToCameraType('user'), equals(CameraType.user), ); }); @@ -681,7 +686,7 @@ void main() { 'returns environment ' 'when the facing mode is environment', (tester) async { expect( - settings.mapFacingModeToCameraType('environment'), + cameraService.mapFacingModeToCameraType('environment'), equals(CameraType.environment), ); }); @@ -690,7 +695,7 @@ void main() { 'returns user ' 'when the facing mode is left', (tester) async { expect( - settings.mapFacingModeToCameraType('left'), + cameraService.mapFacingModeToCameraType('left'), equals(CameraType.user), ); }); @@ -699,7 +704,7 @@ void main() { 'returns user ' 'when the facing mode is right', (tester) async { expect( - settings.mapFacingModeToCameraType('right'), + cameraService.mapFacingModeToCameraType('right'), equals(CameraType.user), ); }); @@ -710,7 +715,7 @@ void main() { 'returns 3840x2160 ' 'when the resolution preset is max', (tester) async { expect( - settings.mapResolutionPresetToSize(ResolutionPreset.max), + cameraService.mapResolutionPresetToSize(ResolutionPreset.max), equals(Size(3840, 2160)), ); }); @@ -719,7 +724,7 @@ void main() { 'returns 3840x2160 ' 'when the resolution preset is ultraHigh', (tester) async { expect( - settings.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), + cameraService.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), equals(Size(3840, 2160)), ); }); @@ -728,7 +733,7 @@ void main() { 'returns 1920x1080 ' 'when the resolution preset is veryHigh', (tester) async { expect( - settings.mapResolutionPresetToSize(ResolutionPreset.veryHigh), + cameraService.mapResolutionPresetToSize(ResolutionPreset.veryHigh), equals(Size(1920, 1080)), ); }); @@ -737,7 +742,7 @@ void main() { 'returns 1280x720 ' 'when the resolution preset is high', (tester) async { expect( - settings.mapResolutionPresetToSize(ResolutionPreset.high), + cameraService.mapResolutionPresetToSize(ResolutionPreset.high), equals(Size(1280, 720)), ); }); @@ -746,7 +751,7 @@ void main() { 'returns 720x480 ' 'when the resolution preset is medium', (tester) async { expect( - settings.mapResolutionPresetToSize(ResolutionPreset.medium), + cameraService.mapResolutionPresetToSize(ResolutionPreset.medium), equals(Size(720, 480)), ); }); @@ -755,7 +760,7 @@ void main() { 'returns 320x240 ' 'when the resolution preset is low', (tester) async { expect( - settings.mapResolutionPresetToSize(ResolutionPreset.low), + cameraService.mapResolutionPresetToSize(ResolutionPreset.low), equals(Size(320, 240)), ); }); @@ -766,7 +771,7 @@ void main() { 'returns portraitPrimary ' 'when the device orientation is portraitUp', (tester) async { expect( - settings.mapDeviceOrientationToOrientationType( + cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.portraitUp, ), equals(OrientationType.portraitPrimary), @@ -777,7 +782,7 @@ void main() { 'returns landscapePrimary ' 'when the device orientation is landscapeLeft', (tester) async { expect( - settings.mapDeviceOrientationToOrientationType( + cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.landscapeLeft, ), equals(OrientationType.landscapePrimary), @@ -788,7 +793,7 @@ void main() { 'returns portraitSecondary ' 'when the device orientation is portraitDown', (tester) async { expect( - settings.mapDeviceOrientationToOrientationType( + cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.portraitDown, ), equals(OrientationType.portraitSecondary), @@ -799,7 +804,7 @@ void main() { 'returns landscapeSecondary ' 'when the device orientation is landscapeRight', (tester) async { expect( - settings.mapDeviceOrientationToOrientationType( + cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.landscapeRight, ), equals(OrientationType.landscapeSecondary), @@ -812,7 +817,7 @@ void main() { 'returns portraitUp ' 'when the orientation type is portraitPrimary', (tester) async { expect( - settings.mapOrientationTypeToDeviceOrientation( + cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.portraitPrimary, ), equals(DeviceOrientation.portraitUp), @@ -823,7 +828,7 @@ void main() { 'returns landscapeLeft ' 'when the orientation type is landscapePrimary', (tester) async { expect( - settings.mapOrientationTypeToDeviceOrientation( + cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.landscapePrimary, ), equals(DeviceOrientation.landscapeLeft), @@ -834,7 +839,7 @@ void main() { 'returns portraitDown ' 'when the orientation type is portraitSecondary', (tester) async { expect( - settings.mapOrientationTypeToDeviceOrientation( + cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.portraitSecondary, ), equals(DeviceOrientation.portraitDown), @@ -845,7 +850,7 @@ void main() { 'returns portraitDown ' 'when the orientation type is portraitSecondary', (tester) async { expect( - settings.mapOrientationTypeToDeviceOrientation( + cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.portraitSecondary, ), equals(DeviceOrientation.portraitDown), @@ -856,7 +861,7 @@ void main() { 'returns landscapeRight ' 'when the orientation type is landscapeSecondary', (tester) async { expect( - settings.mapOrientationTypeToDeviceOrientation( + cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.landscapeSecondary, ), equals(DeviceOrientation.landscapeRight), @@ -867,7 +872,7 @@ void main() { 'returns portraitUp ' 'for an unknown orientation type', (tester) async { expect( - settings.mapOrientationTypeToDeviceOrientation( + cameraService.mapOrientationTypeToDeviceOrientation( 'unknown', ), equals(DeviceOrientation.portraitUp), diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 03ffe81cad64..5c3d842502ba 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -7,7 +7,7 @@ import 'dart:ui'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; -import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/camera_service.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -26,7 +26,7 @@ void main() { late MediaDevices mediaDevices; late MediaStream mediaStream; - late CameraSettings cameraSettings; + late CameraService cameraService; setUp(() { window = MockWindow(); @@ -36,13 +36,13 @@ void main() { when(() => window.navigator).thenReturn(navigator); when(() => navigator.mediaDevices).thenReturn(mediaDevices); - cameraSettings = MockCameraSettings(); + cameraService = MockCameraService(); final videoElement = getVideoElementWithBlankStream(Size(10, 10)); mediaStream = videoElement.captureStream(); when( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( any(), cameraId: any(named: 'cameraId'), ), @@ -55,7 +55,7 @@ void main() { group('initialize', () { testWidgets( - 'calls CameraSettings.getMediaStreamForOptions ' + 'calls CameraService.getMediaStreamForOptions ' 'with provided options', (tester) async { final options = CameraOptions( video: VideoConstraints( @@ -67,13 +67,13 @@ void main() { final camera = Camera( textureId: textureId, options: options, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); verify( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( options, cameraId: textureId, ), @@ -90,7 +90,7 @@ void main() { options: CameraOptions( audio: audioConstraints, ), - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -115,7 +115,7 @@ void main() { 'with correct properties', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -128,7 +128,7 @@ void main() { testWidgets('initializes the camera stream', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -138,16 +138,15 @@ void main() { testWidgets( 'throws an exception ' - 'when CameraSettings.getMediaStreamForOptions throws', - (tester) async { + 'when CameraService.getMediaStreamForOptions throws', (tester) async { final exception = Exception('A media stream exception occured.'); - when(() => cameraSettings.getMediaStreamForOptions(any(), + when(() => cameraService.getMediaStreamForOptions(any(), cameraId: any(named: 'cameraId'))).thenThrow(exception); final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); expect( @@ -163,7 +162,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -180,7 +179,7 @@ void main() { testWidgets( 'initializes the camera stream ' - 'from CameraSettings.getMediaStreamForOptions ' + 'from CameraService.getMediaStreamForOptions ' 'if it does not exist', (tester) async { final options = CameraOptions( video: VideoConstraints( @@ -191,7 +190,7 @@ void main() { final camera = Camera( textureId: textureId, options: options, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -204,7 +203,7 @@ void main() { // Should be called twice: for initialize and play. verify( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( options, cameraId: textureId, ), @@ -219,7 +218,7 @@ void main() { testWidgets('resets the camera stream', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -236,7 +235,7 @@ void main() { testWidgets('returns a captured picture', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -272,7 +271,7 @@ void main() { testWidgets('if the flash mode is auto', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream @@ -307,7 +306,7 @@ void main() { testWidgets('if the flash mode is always', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream @@ -352,7 +351,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -372,7 +371,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -409,7 +408,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream; @@ -437,7 +436,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream; @@ -468,7 +467,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream; @@ -494,7 +493,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream; @@ -531,7 +530,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream; @@ -568,7 +567,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream; @@ -604,7 +603,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, )..window = window; expect( @@ -631,11 +630,11 @@ void main() { group('getMaxZoomLevel', () { testWidgets( 'returns maximum ' - 'from CameraSettings.getZoomLevelCapabilityForCamera', + 'from CameraService.getZoomLevelCapabilityForCamera', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); final zoomLevelCapability = ZoomLevelCapability( @@ -644,12 +643,12 @@ void main() { videoTrack: MockMediaStreamTrack(), ); - when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .thenReturn(zoomLevelCapability); final maximumZoomLevel = camera.getMaxZoomLevel(); - verify(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + verify(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .called(1); expect( @@ -662,11 +661,11 @@ void main() { group('getMinZoomLevel', () { testWidgets( 'returns minimum ' - 'from CameraSettings.getZoomLevelCapabilityForCamera', + 'from CameraService.getZoomLevelCapabilityForCamera', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); final zoomLevelCapability = ZoomLevelCapability( @@ -675,12 +674,12 @@ void main() { videoTrack: MockMediaStreamTrack(), ); - when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .thenReturn(zoomLevelCapability); final minimumZoomLevel = camera.getMinZoomLevel(); - verify(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + verify(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .called(1); expect( @@ -693,11 +692,11 @@ void main() { group('setZoomLevel', () { testWidgets( 'applies zoom on the video track ' - 'from CameraSettings.getZoomLevelCapabilityForCamera', + 'from CameraService.getZoomLevelCapabilityForCamera', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); final videoTrack = MockMediaStreamTrack(); @@ -711,7 +710,7 @@ void main() { when(() => videoTrack.applyConstraints(any())) .thenAnswer((_) async {}); - when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .thenReturn(zoomLevelCapability); const zoom = 75.0; @@ -735,7 +734,7 @@ void main() { 'when the provided zoom level is below minimum', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); final zoomLevelCapability = ZoomLevelCapability( @@ -744,7 +743,7 @@ void main() { videoTrack: MockMediaStreamTrack(), ); - when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .thenReturn(zoomLevelCapability); expect( @@ -769,7 +768,7 @@ void main() { 'when the provided zoom level is below minimum', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); final zoomLevelCapability = ZoomLevelCapability( @@ -778,7 +777,7 @@ void main() { videoTrack: MockMediaStreamTrack(), ); - when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .thenReturn(zoomLevelCapability); expect( @@ -805,7 +804,7 @@ void main() { testWidgets('returns a correct view type', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -821,7 +820,7 @@ void main() { testWidgets('resets the video element\'s source', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); diff --git a/packages/camera/camera_web/test/types/camera_web_exception_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_exception_test.dart similarity index 80% rename from packages/camera/camera_web/test/types/camera_web_exception_test.dart rename to packages/camera/camera_web/example/integration_test/camera_web_exception_test.dart index d58512b460e2..6f8531b6f4af 100644 --- a/packages/camera/camera_web/test/types/camera_web_exception_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_exception_test.dart @@ -4,10 +4,13 @@ import 'package:camera_web/src/types/types.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + group('CameraWebException', () { - test('sets all properties', () { + testWidgets('sets all properties', (tester) async { final cameraId = 1; final code = CameraErrorCode.notFound; final description = 'The camera is not found.'; @@ -19,7 +22,7 @@ void main() { expect(exception.description, equals(description)); }); - test('toString includes all properties', () { + testWidgets('toString includes all properties', (tester) async { final cameraId = 2; final code = CameraErrorCode.notReadable; final description = 'The camera is not readable.'; diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index eb988f49ab87..ada5c2da1a29 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -10,10 +10,10 @@ import 'package:async/async.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/camera_web.dart'; import 'package:camera_web/src/camera.dart'; -import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/camera_service.dart'; import 'package:camera_web/src/types/types.dart'; -import 'package:flutter/widgets.dart' as widgets; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart' as widgets; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -35,7 +35,7 @@ void main() { late Document document; late Element documentElement; - late CameraSettings cameraSettings; + late CameraService cameraService; setUp(() async { window = MockWindow(); @@ -59,10 +59,10 @@ void main() { when(() => document.documentElement).thenReturn(documentElement); when(() => window.document).thenReturn(document); - cameraSettings = MockCameraSettings(); + cameraService = MockCameraService(); when( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( any(), cameraId: any(named: 'cameraId'), ), @@ -71,7 +71,7 @@ void main() { ); CameraPlatform.instance = CameraPlugin( - cameraSettings: cameraSettings, + cameraService: cameraService, )..window = window; }); @@ -88,7 +88,7 @@ void main() { group('availableCameras', () { setUp(() { when( - () => cameraSettings.getFacingModeForVideoTrack( + () => cameraService.getFacingModeForVideoTrack( any(), ), ).thenReturn(null); @@ -102,7 +102,7 @@ void main() { final _ = await CameraPlatform.instance.availableCameras(); verify( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions( audio: AudioConstraints(enabled: true), ), @@ -126,7 +126,7 @@ void main() { final _ = await CameraPlatform.instance.availableCameras(); verify( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions( video: VideoConstraints( deviceId: videoDevice.deviceId, @@ -153,7 +153,7 @@ void main() { final _ = await CameraPlatform.instance.availableCameras(); verifyNever( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions( video: VideoConstraints( deviceId: videoDevice.deviceId, @@ -177,7 +177,7 @@ void main() { FakeMediaStream([MockMediaStreamTrack(), MockMediaStreamTrack()]); when( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions( video: VideoConstraints(deviceId: videoDevice.deviceId), ), @@ -191,7 +191,7 @@ void main() { final _ = await CameraPlatform.instance.availableCameras(); verify( - () => cameraSettings.getFacingModeForVideoTrack( + () => cameraService.getFacingModeForVideoTrack( videoStream.getVideoTracks().first, ), ).called(1); @@ -239,46 +239,46 @@ void main() { ]), ); - // Mock camera settings to return the first video stream + // Mock camera service to return the first video stream // for the first video device. when( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions( video: VideoConstraints(deviceId: firstVideoDevice.deviceId), ), ), ).thenAnswer((_) => Future.value(firstVideoStream)); - // Mock camera settings to return the second video stream + // Mock camera service to return the second video stream // for the second video device. when( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions( video: VideoConstraints(deviceId: secondVideoDevice.deviceId), ), ), ).thenAnswer((_) => Future.value(secondVideoStream)); - // Mock camera settings to return a user facing mode + // Mock camera service to return a user facing mode // for the first video stream. when( - () => cameraSettings.getFacingModeForVideoTrack( + () => cameraService.getFacingModeForVideoTrack( firstVideoStream.getVideoTracks().first, ), ).thenReturn('user'); - when(() => cameraSettings.mapFacingModeToLensDirection('user')) + when(() => cameraService.mapFacingModeToLensDirection('user')) .thenReturn(CameraLensDirection.front); - // Mock camera settings to return an environment facing mode + // Mock camera service to return an environment facing mode // for the second video stream. when( - () => cameraSettings.getFacingModeForVideoTrack( + () => cameraService.getFacingModeForVideoTrack( secondVideoStream.getVideoTracks().first, ), ).thenReturn('environment'); - when(() => cameraSettings.mapFacingModeToLensDirection('environment')) + when(() => cameraService.mapFacingModeToLensDirection('environment')) .thenReturn(CameraLensDirection.back); final cameras = await CameraPlatform.instance.availableCameras(); @@ -318,7 +318,7 @@ void main() { ); when( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions( video: VideoConstraints(deviceId: videoDevice.deviceId), ), @@ -326,12 +326,12 @@ void main() { ).thenAnswer((_) => Future.value(videoStream)); when( - () => cameraSettings.getFacingModeForVideoTrack( + () => cameraService.getFacingModeForVideoTrack( videoStream.getVideoTracks().first, ), ).thenReturn('left'); - when(() => cameraSettings.mapFacingModeToLensDirection('left')) + when(() => cameraService.mapFacingModeToLensDirection('left')) .thenReturn(CameraLensDirection.external); final camera = (await CameraPlatform.instance.availableCameras()).first; @@ -384,7 +384,7 @@ void main() { }); testWidgets( - 'when CameraSettings.getMediaStreamForOptions ' + 'when CameraService.getMediaStreamForOptions ' 'throws CameraWebException', (tester) async { final exception = CameraWebException( cameraId, @@ -392,7 +392,7 @@ void main() { 'description', ); - when(() => cameraSettings.getMediaStreamForOptions(any())) + when(() => cameraService.getMediaStreamForOptions(any())) .thenThrow(exception); expect( @@ -408,14 +408,14 @@ void main() { }); testWidgets( - 'when CameraSettings.getMediaStreamForOptions ' + 'when CameraService.getMediaStreamForOptions ' 'throws PlatformException', (tester) async { final exception = PlatformException( code: CameraErrorCode.notSupported.toString(), message: 'message', ); - when(() => cameraSettings.getMediaStreamForOptions(any())) + when(() => cameraService.getMediaStreamForOptions(any())) .thenThrow(exception); expect( @@ -454,13 +454,13 @@ void main() { .camerasMetadata[cameraDescription] = cameraMetadata; when( - () => cameraSettings.mapFacingModeToCameraType('user'), + () => cameraService.mapFacingModeToCameraType('user'), ).thenReturn(CameraType.user); }); testWidgets('with appropriate options', (tester) async { when( - () => cameraSettings + () => cameraService .mapResolutionPresetToSize(ResolutionPreset.ultraHigh), ).thenReturn(ultraHighResolutionSize); @@ -503,8 +503,7 @@ void main() { 'and enabled audio set to false ' 'when no options are specified', (tester) async { when( - () => - cameraSettings.mapResolutionPresetToSize(ResolutionPreset.max), + () => cameraService.mapResolutionPresetToSize(ResolutionPreset.max), ).thenReturn(maxResolutionSize); final cameraId = await CameraPlatform.instance.createCamera( @@ -657,7 +656,7 @@ void main() { group('lockCaptureOrientation', () { setUp(() { when( - () => cameraSettings.mapDeviceOrientationToOrientationType(any()), + () => cameraService.mapDeviceOrientationToOrientationType(any()), ).thenReturn(OrientationType.portraitPrimary); }); @@ -676,7 +675,7 @@ void main() { 'locks the capture orientation ' 'based on the given device orientation', (tester) async { when( - () => cameraSettings.mapDeviceOrientationToOrientationType( + () => cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.landscapeRight, ), ).thenReturn(OrientationType.landscapeSecondary); @@ -687,7 +686,7 @@ void main() { ); verify( - () => cameraSettings.mapDeviceOrientationToOrientationType( + () => cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.landscapeRight, ), ).called(1); @@ -785,7 +784,7 @@ void main() { group('unlockCaptureOrientation', () { setUp(() { when( - () => cameraSettings.mapDeviceOrientationToOrientationType(any()), + () => cameraService.mapDeviceOrientationToOrientationType(any()), ).thenReturn(OrientationType.portraitPrimary); }); @@ -1434,7 +1433,7 @@ void main() { 'with an appropriate view type', (tester) async { final camera = Camera( textureId: cameraId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); // Save the camera in the camera plugin. @@ -1560,7 +1559,7 @@ void main() { testWidgets('returns the correct camera', (tester) async { final camera = Camera( textureId: cameraId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); // Save the camera in the camera plugin. @@ -1599,7 +1598,7 @@ void main() { videoElement = getVideoElementWithBlankStream(videoSize); when( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( any(), cameraId: cameraId, ), @@ -1607,7 +1606,7 @@ void main() { final camera = Camera( textureId: cameraId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); // Save the camera in the camera plugin. @@ -1963,13 +1962,13 @@ void main() { 'emits a DeviceOrientationChangedEvent ' 'when the screen orientation is changed', (tester) async { when( - () => cameraSettings.mapOrientationTypeToDeviceOrientation( + () => cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.landscapePrimary, ), ).thenReturn(DeviceOrientation.landscapeLeft); when( - () => cameraSettings.mapOrientationTypeToDeviceOrientation( + () => cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.portraitSecondary, ), ).thenReturn(DeviceOrientation.portraitDown); diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart index 5fa52dd3398d..436f2065aaf5 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -7,7 +7,7 @@ import 'dart:html'; import 'dart:ui'; import 'package:camera_web/src/camera.dart'; -import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/camera_service.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:cross_file/cross_file.dart'; import 'package:mocktail/mocktail.dart'; @@ -26,7 +26,7 @@ class MockNavigator extends Mock implements Navigator {} class MockMediaDevices extends Mock implements MediaDevices {} -class MockCameraSettings extends Mock implements CameraSettings {} +class MockCameraService extends Mock implements CameraService {} class MockMediaStreamTrack extends Mock implements MediaStreamTrack {} diff --git a/packages/camera/camera_web/test/types/zoom_level_capability_test.dart b/packages/camera/camera_web/example/integration_test/zoom_level_capability_test.dart similarity index 80% rename from packages/camera/camera_web/test/types/zoom_level_capability_test.dart rename to packages/camera/camera_web/example/integration_test/zoom_level_capability_test.dart index c382b4b76cc4..09de03100871 100644 --- a/packages/camera/camera_web/test/types/zoom_level_capability_test.dart +++ b/packages/camera/camera_web/example/integration_test/zoom_level_capability_test.dart @@ -4,12 +4,15 @@ import 'package:camera_web/src/types/types.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; -import '../helpers/helpers.dart'; +import 'helpers/helpers.dart'; void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + group('ZoomLevelCapability', () { - test('sets all properties', () { + testWidgets('sets all properties', (tester) async { const minimum = 100.0; const maximum = 400.0; final videoTrack = MockMediaStreamTrack(); @@ -25,7 +28,7 @@ void main() { expect(capability.videoTrack, equals(videoTrack)); }); - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { final videoTrack = MockMediaStreamTrack(); expect( diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index c77d36023058..6f758843a047 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -6,7 +6,7 @@ import 'dart:html' as html; import 'dart:ui'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/camera_service.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/foundation.dart'; @@ -18,7 +18,7 @@ String _getViewType(int cameraId) => 'plugins.flutter.io/camera_$cameraId'; /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices /// /// The obtained camera stream is constrained by [options] and fetched -/// with [CameraSettings.getMediaStreamForOptions]. +/// with [CameraService.getMediaStreamForOptions]. /// /// The camera stream is displayed in the [videoElement] wrapped in the /// [divElement] to avoid overriding the custom styles applied to @@ -40,9 +40,9 @@ class Camera { /// [options] and [window]. Camera({ required this.textureId, - required CameraSettings cameraSettings, + required CameraService cameraService, this.options = const CameraOptions(), - }) : _cameraSettings = cameraSettings; + }) : _cameraService = cameraService; // A torch mode constraint name. // See: https://w3c.github.io/mediacapture-image/#dom-mediatracksupportedconstraints-torch @@ -71,8 +71,8 @@ class Camera { @visibleForTesting FlashMode? flashMode; - /// The camera settings used to get the media stream for the camera. - final CameraSettings _cameraSettings; + /// The camera service used to get the media stream for the camera. + final CameraService _cameraService; /// The current browser window used to access media devices. @visibleForTesting @@ -81,7 +81,7 @@ class Camera { /// Initializes the camera stream displayed in the [videoElement]. /// Registers the camera view with [textureId] under [_getViewType] type. Future initialize() async { - stream = await _cameraSettings.getMediaStreamForOptions( + stream = await _cameraService.getMediaStreamForOptions( options, cameraId: textureId, ); @@ -110,7 +110,7 @@ class Camera { /// Initializes the camera source if the camera was previously stopped. Future play() async { if (videoElement.srcObject == null) { - stream = await _cameraSettings.getMediaStreamForOptions( + stream = await _cameraService.getMediaStreamForOptions( options, cameraId: textureId, ); @@ -251,14 +251,14 @@ class Camera { /// Throws a [CameraWebException] if the zoom level is not supported /// or the camera has not been initialized or started. double getMaxZoomLevel() => - _cameraSettings.getZoomLevelCapabilityForCamera(this).maximum; + _cameraService.getZoomLevelCapabilityForCamera(this).maximum; /// Returns the camera minimum zoom level. /// /// Throws a [CameraWebException] if the zoom level is not supported /// or the camera has not been initialized or started. double getMinZoomLevel() => - _cameraSettings.getZoomLevelCapabilityForCamera(this).minimum; + _cameraService.getZoomLevelCapabilityForCamera(this).minimum; /// Sets the camera zoom level to [zoom]. /// @@ -266,7 +266,7 @@ class Camera { /// not supported or the camera has not been initialized or started. void setZoomLevel(double zoom) { final zoomLevelCapability = - _cameraSettings.getZoomLevelCapabilityForCamera(this); + _cameraService.getZoomLevelCapabilityForCamera(this); if (zoom < zoomLevelCapability.minimum || zoom > zoomLevelCapability.maximum) { diff --git a/packages/camera/camera_web/lib/src/camera_settings.dart b/packages/camera/camera_web/lib/src/camera_service.dart similarity index 99% rename from packages/camera/camera_web/lib/src/camera_settings.dart rename to packages/camera/camera_web/lib/src/camera_service.dart index 7d35fff84112..c1a4ad1038ab 100644 --- a/packages/camera/camera_web/lib/src/camera_settings.dart +++ b/packages/camera/camera_web/lib/src/camera_service.dart @@ -12,9 +12,9 @@ import 'package:camera_web/src/types/types.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -/// A utility to fetch, map camera settings and +/// A service to fetch, map camera settings and /// obtain the camera stream. -class CameraSettings { +class CameraService { // A facing mode constraint name. static const _facingModeKey = "facingMode"; diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index ecea3a76e74a..fda33996f474 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -8,7 +8,7 @@ import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; -import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/camera_service.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -25,18 +25,18 @@ const String _kDefaultErrorMessage = /// This class implements the `package:camera` functionality for the web. class CameraPlugin extends CameraPlatform { /// Creates a new instance of [CameraPlugin] - /// with the given [cameraSettings] utility. - CameraPlugin({required CameraSettings cameraSettings}) - : _cameraSettings = cameraSettings; + /// with the given [cameraService]. + CameraPlugin({required CameraService cameraService}) + : _cameraService = cameraService; /// Registers this class as the default instance of [CameraPlatform]. static void registerWith(Registrar registrar) { CameraPlatform.instance = CameraPlugin( - cameraSettings: CameraSettings(), + cameraService: CameraService(), ); } - final CameraSettings _cameraSettings; + final CameraService _cameraService; /// The cameras managed by the [CameraPlugin]. @visibleForTesting @@ -86,7 +86,7 @@ class CameraPlugin extends CameraPlatform { } // Request video and audio permissions. - await _cameraSettings.getMediaStreamForOptions( + await _cameraService.getMediaStreamForOptions( CameraOptions( audio: AudioConstraints(enabled: true), ), @@ -121,13 +121,13 @@ class CameraPlugin extends CameraPlatform { if (videoTracks.isNotEmpty) { // Get the facing mode from the first available video track. final facingMode = - _cameraSettings.getFacingModeForVideoTrack(videoTracks.first); + _cameraService.getFacingModeForVideoTrack(videoTracks.first); // Get the lens direction based on the facing mode. // Fallback to the external lens direction // if the facing mode is not available. final lensDirection = facingMode != null - ? _cameraSettings.mapFacingModeToLensDirection(facingMode) + ? _cameraService.mapFacingModeToLensDirection(facingMode) : CameraLensDirection.external; // Create a camera description. @@ -191,20 +191,19 @@ class CameraPlugin extends CameraPlatform { final cameraMetadata = camerasMetadata[cameraDescription]!; final cameraType = cameraMetadata.facingMode != null - ? _cameraSettings - .mapFacingModeToCameraType(cameraMetadata.facingMode!) + ? _cameraService.mapFacingModeToCameraType(cameraMetadata.facingMode!) : null; // Use the highest resolution possible // if the resolution preset is not specified. - final videoSize = _cameraSettings + final videoSize = _cameraService .mapResolutionPresetToSize(resolutionPreset ?? ResolutionPreset.max); // Create a camera with the given audio and video constraints. // Sensor orientation is currently not supported. final camera = Camera( textureId: textureId, - cameraSettings: _cameraSettings, + cameraService: _cameraService, options: CameraOptions( audio: AudioConstraints(enabled: enableAudio), video: VideoConstraints( @@ -334,7 +333,7 @@ class CameraPlugin extends CameraPlatform { if (orientation != null) { return orientation.onChange.map( (html.Event _) { - final deviceOrientation = _cameraSettings + final deviceOrientation = _cameraService .mapOrientationTypeToDeviceOrientation(orientation.type!); return DeviceOrientationChangedEvent(deviceOrientation); }, @@ -354,7 +353,7 @@ class CameraPlugin extends CameraPlatform { final documentElement = window?.document.documentElement; if (orientation != null && documentElement != null) { - final orientationType = _cameraSettings + final orientationType = _cameraService .mapDeviceOrientationToOrientationType(deviceOrientation); // Full-screen mode may be required to modify the device orientation. @@ -549,7 +548,7 @@ class CameraPlugin extends CameraPlatform { video: VideoConstraints(deviceId: deviceId), ); - return _cameraSettings.getMediaStreamForOptions(cameraOptions); + return _cameraService.getMediaStreamForOptions(cameraOptions); } /// Returns a camera for the given [cameraId]. diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index ec674f375164..a2aa43c22d65 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -30,5 +30,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - mocktail: ^0.1.4 pedantic: ^1.11.1 \ No newline at end of file diff --git a/packages/camera/camera_web/test/helpers/helpers.dart b/packages/camera/camera_web/test/helpers/helpers.dart deleted file mode 100644 index 7094f55bb62e..000000000000 --- a/packages/camera/camera_web/test/helpers/helpers.dart +++ /dev/null @@ -1,5 +0,0 @@ -// 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. - -export 'mocks.dart'; diff --git a/packages/camera/camera_web/test/helpers/mocks.dart b/packages/camera/camera_web/test/helpers/mocks.dart deleted file mode 100644 index 34c56632b60f..000000000000 --- a/packages/camera/camera_web/test/helpers/mocks.dart +++ /dev/null @@ -1,20 +0,0 @@ -// 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 'dart:html'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockMediaStreamTrack extends Mock implements MediaStreamTrack {} - -/// A fake [MediaError] that returns the provided error [_code]. -class FakeMediaError extends Fake implements MediaError { - FakeMediaError(this._code); - - final int _code; - - @override - int get code => _code; -} From d6ba532344a62b19026a8d4f25a70fc9bdc10e95 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 12 Aug 2021 02:22:05 +0200 Subject: [PATCH 063/123] fix: don't request full-screen mode in unlockCaptureOrientation (#4226) --- .../example/integration_test/camera_web_test.dart | 10 ---------- packages/camera/camera_web/lib/src/camera_web.dart | 3 --- 2 files changed, 13 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index ada5c2da1a29..555e20040a3c 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -788,16 +788,6 @@ void main() { ).thenReturn(OrientationType.portraitPrimary); }); - testWidgets( - 'requests full-screen mode ' - 'on documentElement', (tester) async { - await CameraPlatform.instance.unlockCaptureOrientation( - cameraId, - ); - - verify(documentElement.requestFullscreen).called(1); - }); - testWidgets('unlocks the capture orientation', (tester) async { await CameraPlatform.instance.unlockCaptureOrientation( cameraId, diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index fda33996f474..1038d227e23e 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -378,9 +378,6 @@ class CameraPlugin extends CameraPlatform { final documentElement = window?.document.documentElement; if (orientation != null && documentElement != null) { - // Full-screen mode may be required to modify the device orientation. - // See: https://w3c.github.io/screen-orientation/#interaction-with-fullscreen-api - documentElement.requestFullscreen(); orientation.unlock(); } else { throw PlatformException( From 1ac46e159c5d560df0cb716b36e622ad4f8f188c Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 12 Aug 2021 02:24:52 +0200 Subject: [PATCH 064/123] [camera_web] docs: add `setFlashMode` comments (#4227) --- packages/camera/camera_web/lib/src/camera.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 6f758843a047..237f9858855e 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -133,8 +133,8 @@ class Camera { /// Captures a picture and returns the saved file in a JPEG format. /// - /// Enables the device flash when taking a picture if the flash mode - /// is either [FlashMode.auto] or [FlashMode.always]. + /// Enables the camera flash (torch mode) for a period of taking a picture + /// if the flash mode is either [FlashMode.auto] or [FlashMode.always]. Future takePicture() async { final shouldEnableTorchMode = flashMode == FlashMode.auto || flashMode == FlashMode.always; @@ -185,7 +185,14 @@ class Camera { } } - /// Sets the camera flash mode to [mode]. + /// Sets the camera flash mode to [mode] by modifying the camera + /// torch mode constraint. + /// + /// The torch mode is enabled for [FlashMode.torch] and + /// disabled for [FlashMode.off]. + /// + /// For [FlashMode.auto] and [FlashMode.always] the torch mode is enabled + /// only for a period of taking a picture in [takePicture]. /// /// Throws a [CameraWebException] if the torch mode is not supported /// or the camera has not been initialized or started. From 4383bb15d91384efb22dbe409c3fd58e4552267c Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 12 Aug 2021 02:27:05 +0200 Subject: [PATCH 065/123] [camera_web] Handle camera errors in `takePicture` (#4230) --- .../integration_test/camera_web_test.dart | 62 +++++++++++++++++++ .../camera/camera_web/lib/src/camera_web.dart | 3 + 2 files changed, 65 insertions(+) diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 555e20040a3c..57c3de83ba04 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -929,6 +929,32 @@ void main() { ), ); }); + + testWidgets('when takePicture throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.takePicture).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); }); }); @@ -1763,6 +1789,42 @@ void main() { await streamQueue.cancel(); }); + testWidgets( + 'emits a CameraErrorEvent ' + 'on takePicture error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.takePicture).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + testWidgets( 'emits a CameraErrorEvent ' 'on setFlashMode error', (tester) async { diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 1038d227e23e..134e0726ba4b 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -396,6 +396,9 @@ class CameraPlugin extends CameraPlatform { return getCamera(cameraId).takePicture(); } on html.DomException catch (e) { throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); } } From e5fc83d2b9024802e3e7712854b9789d7cbcee3e Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 13 Aug 2021 01:40:58 +0200 Subject: [PATCH 066/123] [camera_web] Fix `getCapabilities` not supported error thrown when selecting a camera on Firefox (#4234) * feat: check if getCapabilities can be called rather than catching an exception --- .../integration_test/camera_service_test.dart | 86 ++++++++----------- .../integration_test/helpers/mocks.dart | 3 + .../camera_web/lib/src/camera_service.dart | 68 +++++++-------- .../lib/src/shims/dart_js_util.dart | 14 +++ 4 files changed, 85 insertions(+), 86 deletions(-) create mode 100644 packages/camera/camera_web/lib/src/shims/dart_js_util.dart diff --git a/packages/camera/camera_web/example/integration_test/camera_service_test.dart b/packages/camera/camera_web/example/integration_test/camera_service_test.dart index 161aeb4a595e..937f023f4b36 100644 --- a/packages/camera/camera_web/example/integration_test/camera_service_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_service_test.dart @@ -9,6 +9,7 @@ import 'dart:js_util' as js_util; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_service.dart'; +import 'package:camera_web/src/shims/dart_js_util.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -27,15 +28,25 @@ void main() { late Navigator navigator; late MediaDevices mediaDevices; late CameraService cameraService; + late JsUtil jsUtil; setUp(() async { window = MockWindow(); navigator = MockNavigator(); mediaDevices = MockMediaDevices(); + jsUtil = MockJsUtil(); when(() => window.navigator).thenReturn(navigator); when(() => navigator.mediaDevices).thenReturn(mediaDevices); + // Mock JsUtil to return the real getProperty from dart:js_util. + when(() => jsUtil.getProperty(any(), any())).thenAnswer( + (invocation) => js_util.getProperty( + invocation.positionalArguments[0], + invocation.positionalArguments[1], + ), + ); + cameraService = CameraService()..window = window; }); @@ -354,6 +365,8 @@ void main() { when(() => camera.textureId).thenReturn(0); when(() => camera.stream).thenReturn(FakeMediaStream(videoTracks)); + + cameraService.jsUtil = jsUtil; }); testWidgets( @@ -496,6 +509,10 @@ void main() { }); group('getFacingModeForVideoTrack', () { + setUp(() { + cameraService.jsUtil = jsUtil; + }); + testWidgets( 'throws PlatformException ' 'with notSupported error ' @@ -525,14 +542,18 @@ void main() { final facingMode = cameraService.getFacingModeForVideoTrack(MockMediaStreamTrack()); - expect( - facingMode, - equals(null), - ); + expect(facingMode, isNull); }); group('when the facing mode is supported', () { + late MediaStreamTrack videoTrack; + setUp(() { + videoTrack = MockMediaStreamTrack(); + + when(() => jsUtil.hasProperty(videoTrack, 'getCapabilities')) + .thenReturn(true); + when(mediaDevices.getSupportedConstraints).thenReturn({ 'facingMode': true, }); @@ -541,95 +562,58 @@ void main() { testWidgets( 'returns an appropriate facing mode ' 'based on the video track settings', (tester) async { - final videoTrack = MockMediaStreamTrack(); - when(videoTrack.getSettings).thenReturn({'facingMode': 'user'}); final facingMode = cameraService.getFacingModeForVideoTrack(videoTrack); - expect( - facingMode, - equals('user'), - ); + expect(facingMode, equals('user')); }); testWidgets( 'returns an appropriate facing mode ' 'based on the video track capabilities ' 'when the facing mode setting is empty', (tester) async { - final videoTrack = MockMediaStreamTrack(); - when(videoTrack.getSettings).thenReturn({}); when(videoTrack.getCapabilities).thenReturn({ 'facingMode': ['environment', 'left'] }); + when(() => jsUtil.hasProperty(videoTrack, 'getCapabilities')) + .thenReturn(true); + final facingMode = cameraService.getFacingModeForVideoTrack(videoTrack); - expect( - facingMode, - equals('environment'), - ); + expect(facingMode, equals('environment')); }); testWidgets( 'returns null ' 'when the facing mode setting ' 'and capabilities are empty', (tester) async { - final videoTrack = MockMediaStreamTrack(); - when(videoTrack.getSettings).thenReturn({}); when(videoTrack.getCapabilities).thenReturn({'facingMode': []}); final facingMode = cameraService.getFacingModeForVideoTrack(videoTrack); - expect( - facingMode, - equals(null), - ); + expect(facingMode, isNull); }); testWidgets( 'returns null ' 'when the facing mode setting is empty and ' 'the video track capabilities are not supported', (tester) async { - final videoTrack = MockMediaStreamTrack(); - when(videoTrack.getSettings).thenReturn({}); - when(videoTrack.getCapabilities).thenThrow(JSNoSuchMethodError()); + + when(() => jsUtil.hasProperty(videoTrack, 'getCapabilities')) + .thenReturn(false); final facingMode = cameraService.getFacingModeForVideoTrack(videoTrack); - expect( - facingMode, - equals(null), - ); - }); - - testWidgets( - 'throws PlatformException ' - 'with unknown error ' - 'when getting the video track capabilities ' - 'throws an unknown error', (tester) async { - final videoTrack = MockMediaStreamTrack(); - - when(videoTrack.getSettings).thenReturn({}); - when(videoTrack.getCapabilities).thenThrow(Exception('Unknown')); - - expect( - () => cameraService.getFacingModeForVideoTrack(videoTrack), - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCode.unknown.toString(), - ), - ), - ); + expect(facingMode, isNull); }); }); }); diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart index 436f2065aaf5..e6a11cc0b454 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -8,6 +8,7 @@ import 'dart:ui'; import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_service.dart'; +import 'package:camera_web/src/shims/dart_js_util.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:cross_file/cross_file.dart'; import 'package:mocktail/mocktail.dart'; @@ -38,6 +39,8 @@ class MockVideoElement extends Mock implements VideoElement {} class MockXFile extends Mock implements XFile {} +class MockJsUtil extends Mock implements JsUtil {} + /// A fake [MediaStream] that returns the provided [_videoTracks]. class FakeMediaStream extends Fake implements MediaStream { FakeMediaStream(this._videoTracks); diff --git a/packages/camera/camera_web/lib/src/camera_service.dart b/packages/camera/camera_web/lib/src/camera_service.dart index c1a4ad1038ab..612b2b138fdb 100644 --- a/packages/camera/camera_web/lib/src/camera_service.dart +++ b/packages/camera/camera_web/lib/src/camera_service.dart @@ -4,10 +4,10 @@ import 'dart:html' as html; import 'dart:ui'; -import 'dart:js_util' as js_util; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/shims/dart_js_util.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -22,6 +22,10 @@ class CameraService { @visibleForTesting html.Window? window = html.window; + /// The utility to manipulate JavaScript interop objects. + @visibleForTesting + JsUtil jsUtil = JsUtil(); + /// Returns a media stream associated with the camera device /// with [cameraId] and constrained by [options]. Future getMediaStreamForOptions( @@ -143,8 +147,8 @@ class CameraService { // The zoom level capability is a nested JS object, therefore // we need to access its properties with the js_util library. // See: https://api.dart.dev/stable/2.13.4/dart-js_util/getProperty.html - final minimumZoomLevel = js_util.getProperty(zoomLevelCapability, 'min'); - final maximumZoomLevel = js_util.getProperty(zoomLevelCapability, 'max'); + final minimumZoomLevel = jsUtil.getProperty(zoomLevelCapability, 'min'); + final maximumZoomLevel = jsUtil.getProperty(zoomLevelCapability, 'max'); if (minimumZoomLevel != null && maximumZoomLevel != null) { return ZoomLevelCapability( @@ -201,40 +205,34 @@ class CameraService { final facingMode = videoTrackSettings[_facingModeKey]; if (facingMode == null) { - try { - // If the facing mode does not exist in the video track settings, - // check for the facing mode in the video track capabilities. - // - // MediaTrackCapabilities: - // https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackcapabilities - // - // This may throw a not supported error on Firefox. - final videoTrackCapabilities = videoTrack.getCapabilities(); + // If the facing mode does not exist in the video track settings, + // check for the facing mode in the video track capabilities. + // + // MediaTrackCapabilities: + // https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackcapabilities + + // Check if getting the video track capabilities is supported. + // + // The method may not be supported on Firefox. + // See: https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/getCapabilities#browser_compatibility + if (!jsUtil.hasProperty(videoTrack, 'getCapabilities')) { + // Return null if the video track capabilites are not supported. + return null; + } + + final videoTrackCapabilities = videoTrack.getCapabilities(); - // A list of facing mode capabilities as - // the camera may support multiple facing modes. - final facingModeCapabilities = - List.from(videoTrackCapabilities[_facingModeKey] ?? []); + // A list of facing mode capabilities as + // the camera may support multiple facing modes. + final facingModeCapabilities = + List.from(videoTrackCapabilities[_facingModeKey] ?? []); - if (facingModeCapabilities.isNotEmpty) { - final facingModeCapability = facingModeCapabilities.first; - return facingModeCapability; - } else { - // Return null if there are no facing mode capabilities. - return null; - } - } catch (e) { - switch (e.runtimeType.toString()) { - case 'JSNoSuchMethodError': - // Return null if getting capabilities is currently not supported. - return null; - default: - throw PlatformException( - code: CameraErrorCode.unknown.toString(), - message: - 'An unknown error occured when getting the video track capabilities.', - ); - } + if (facingModeCapabilities.isNotEmpty) { + final facingModeCapability = facingModeCapabilities.first; + return facingModeCapability; + } else { + // Return null if there are no facing mode capabilities. + return null; } } diff --git a/packages/camera/camera_web/lib/src/shims/dart_js_util.dart b/packages/camera/camera_web/lib/src/shims/dart_js_util.dart new file mode 100644 index 000000000000..6601bec6f529 --- /dev/null +++ b/packages/camera/camera_web/lib/src/shims/dart_js_util.dart @@ -0,0 +1,14 @@ +// 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 'dart:js_util' as js_util; + +/// A utility that shims dart:js_util to manipulate JavaScript interop objects. +class JsUtil { + /// Returns true if the object [o] has the property [name]. + bool hasProperty(Object o, Object name) => js_util.hasProperty(o, name); + + /// Returns the value of the property [name] in the object [o]. + dynamic getProperty(Object o, Object name) => js_util.getProperty(o, name); +} From 1e497d62039b8d668bb82c2bdde76ecb9e1a6af1 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 13 Aug 2021 01:41:19 +0200 Subject: [PATCH 067/123] [camera_web] Add missing setFlashMode test (#4235) --- .../example/integration_test/camera_web_test.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 57c3de83ba04..6b03b6d77035 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -998,6 +998,21 @@ void main() { }); group('setFlashMode', () { + testWidgets('calls setFlashMode on the camera', (tester) async { + final camera = MockCamera(); + const flashMode = FlashMode.always; + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.setFlashMode( + cameraId, + flashMode, + ); + + verify(() => camera.setFlashMode(flashMode)).called(1); + }); + group('throws PlatformException', () { testWidgets( 'with notFound error ' From b1c65df43c5a0e664ebc75eabd78b16b49f51c39 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 13 Aug 2021 01:41:43 +0200 Subject: [PATCH 068/123] [camera_web] Update the web plugin README (#4237) * docs: update web plugin README * docs: update web plugin missing implementation README --- packages/camera/camera_web/README.md | 76 ++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera_web/README.md b/packages/camera/camera_web/README.md index d57bd7446d17..8c216b3f4e0e 100644 --- a/packages/camera/camera_web/README.md +++ b/packages/camera/camera_web/README.md @@ -1,7 +1,77 @@ # Camera Web Plugin -A Flutter plugin for Web allowing access to the device cameras. +The web implementation of [`camera`][camera]. -*Note*: This plugin is under development. +*Note*: This plugin is under development. See [missing implementation](#missing-implementation). -In order to use this plugin, your app should depend both on `camera` and `camera_web`. This is a temporary solution until a plugin is released. \ No newline at end of file +## Usage + +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), which means you can simply use `camera` normally. This package will be automatically included in your app when you do. + +## Example + +Find the example in the [`camera` package](https://pub.dev/packages/camera#example). + +## Limitations on the web platform + +### Camera devices + +The camera devices are accessed with [Stream Web API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Streams_API) with the following [browser support](https://caniuse.com/stream): + +![Data on support for the Stream feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/stream.png) + +Accessing camera devices requires a [secure browsing context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). This means that you might need to serve your web application over HTTPS. For insecure contexts `CameraPlatform.availableCameras` might throw a `CameraException` with the `permissionDenied` error code. + +### Device orientation + +The device orientation implementation is backed by [`Screen Orientation Web API`](https://www.w3.org/TR/screen-orientation/) with the following [browser support](https://caniuse.com/screen-orientation): + +![Data on support for the Screen Orientation feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/screen-orientation.png) + +For the browsers that do not support the device orientation: +- `CameraPlatform.onDeviceOrientationChanged` returns an empty stream. +- `CameraPlatform.lockCaptureOrientation` and `CameraPlatform.unlockCaptureOrientation` throw a `PlatformException` with the `orientationNotSupported` error code. + +### Flash mode and zoom level + +The flash mode and zoom level implementation is backed by [Image Capture Web API](https://w3c.github.io/mediacapture-image/) with the following [browser support](https://caniuse.com/mdn-api_imagecapture) (as of 12 August 2021): + +![Data on support for the Image Capture feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/static/v1/mdn-api__ImageCapture-1628778966589.png) + +For the browsers that do not support the flash mode: +- `CameraPlatform.setFlashMode` throws a `PlatformException` with the `torchModeNotSupported` error code. + +For the browsers that do not support the zoom level: +- `CameraPlatform.getMaxZoomLevel`, `CameraPlatform.getMinZoomLevel` and `CameraPlatform.setZoomLevel` throw a `PlatformException` with the `zoomLevelNotSupported` error code. + +### Taking a picture + +The image capturing implementation is backed by [`URL.createObjectUrl` Web API](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) with the following [browser support](https://caniuse.com/bloburls): + +![Data on support for the Blob URLs feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/bloburls.png) + +The web platform does not support `dart:io`. Attempts to display a captured image using `Image.file` will throw an error. The capture image contains a network-accessible URL pointing to a location within the browser and should be displayed using `Image.network` or `Image.memory` after loading the image bytes to memory. + +See the example below: + +```dart +if (kIsWeb) { + Image.network(capturedImage.path); +} else { + Image.file(File(capturedImage.path)); +} +``` + +## Missing implementation + +The web implementation of [`camera`][camera] is missing the following features: +- Video recording +- Exposure mode, point and offset +- Focus mode and point +- Camera closing events +- Camera sensor orientation +- Camera image format group +- Camera image streaming + + +[camera]: https://pub.dev/packages/camera \ No newline at end of file From c8570fc681d83c1086f30ecf6843a958f63323ba Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 13 Aug 2021 08:52:06 +0200 Subject: [PATCH 069/123] fix: disposed CameraController error thrown when changing a camera (#4236) --- packages/camera/camera/example/lib/main.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 00ac2251ba2a..2314aecbece3 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -594,17 +594,21 @@ class _CameraExampleHomeState extends State } void onNewCameraSelected(CameraDescription cameraDescription) async { - if (controller != null) { - await controller!.dispose(); - } + final previousCameraController = controller; + final CameraController cameraController = CameraController( cameraDescription, ResolutionPreset.medium, enableAudio: enableAudio, imageFormatGroup: ImageFormatGroup.jpeg, ); + controller = cameraController; + if (mounted) { + setState(() {}); + } + // If the controller is updated then update the UI. cameraController.addListener(() { if (mounted) setState(() {}); @@ -637,6 +641,8 @@ class _CameraExampleHomeState extends State if (mounted) { setState(() {}); } + + await previousCameraController?.dispose(); } void onTakePictureButtonPressed() { From ebf4d59543a998d25c90b4a3f72c2eb42d628b7c Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Sat, 14 Aug 2021 01:04:33 +0200 Subject: [PATCH 070/123] [camera_web] Add support for pausing and resuming the camera preview (#4239) * chore: update camera_platform_interface to 2.1.0 * feat: add pause to Camera * test: add Camera pause test * feat: add pausePreview and resumePreview implementation * test: add pausePreview and resumePreview tests --- .../example/integration_test/camera_test.dart | 18 ++ .../integration_test/camera_web_test.dart | 165 ++++++++++++++++++ .../camera/camera_web/lib/src/camera.dart | 5 + .../camera/camera_web/lib/src/camera_web.dart | 21 +++ packages/camera/camera_web/pubspec.yaml | 2 +- 5 files changed, 210 insertions(+), 1 deletion(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 5c3d842502ba..1d1659352f26 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -214,6 +214,24 @@ void main() { }); }); + group('pause', () { + testWidgets('pauses the camera stream', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + await camera.play(); + + expect(camera.videoElement.paused, isFalse); + + camera.pause(); + + expect(camera.videoElement.paused, isTrue); + }); + }); + group('stop', () { testWidgets('resets the camera stream', (tester) async { final camera = Camera( diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 6b03b6d77035..d48df122277f 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -1459,6 +1459,135 @@ void main() { }); }); + group('pausePreview', () { + testWidgets('calls pause on the camera', (tester) async { + final camera = MockCamera(); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.pausePreview(cameraId); + + verify(camera.pause).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () async => await CameraPlatform.instance.pausePreview(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when pause throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(camera.pause).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.pausePreview(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + + group('resumePreview', () { + testWidgets('calls play on the camera', (tester) async { + final camera = MockCamera(); + + when(camera.play).thenAnswer((_) async => {}); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.resumePreview(cameraId); + + verify(camera.play).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () async => await CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when play throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(camera.play).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when play throws CameraWebException', (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.unknown, + 'description', + ); + + when(camera.play).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + testWidgets( 'buildPreview returns an HtmlElementView ' 'with an appropriate view type', (tester) async { @@ -1993,6 +2122,42 @@ void main() { await streamQueue.cancel(); }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on resumePreview error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.unknown, + 'description', + ); + + when(camera.play).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); }); testWidgets('onVideoRecordedEvent throws UnimplementedError', diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 237f9858855e..c1343ceccf49 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -119,6 +119,11 @@ class Camera { await videoElement.play(); } + /// Pauses the camera stream on the current frame. + void pause() async { + videoElement.pause(); + } + /// Stops the camera stream and resets the camera source. void stop() { final tracks = videoElement.srcObject?.getTracks(); diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 134e0726ba4b..8b131f5d4f6e 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -517,6 +517,27 @@ class CameraPlugin extends CameraPlatform { } } + @override + Future pausePreview(int cameraId) async { + try { + getCamera(cameraId).pause(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } + } + + @override + Future resumePreview(int cameraId) async { + try { + await getCamera(cameraId).play(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } + } + @override Widget buildPreview(int cameraId) { return HtmlElementView( diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index a2aa43c22d65..c4d78999f273 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -21,7 +21,7 @@ flutter: fileName: camera_web.dart dependencies: - camera_platform_interface: ^2.0.1 + camera_platform_interface: ^2.1.0 flutter: sdk: flutter flutter_web_plugins: From 0209a2fadefe2950ffaaac5aa184ccb1d2a18f3b Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 13 Aug 2021 16:22:24 -0700 Subject: [PATCH 071/123] Eliminate build_all_plugins_app.sh (#4232) Removes the `build_all_plugins_app.sh` bash script, in support of the goal of eliminating all use of bash from the repository (for maintainability, and for better Windows compatibility). - The exclusion list moves to a config file, match other recent repo changes - The exclusion logging moves into the tool itself, consistent with the tool doing more logging of skipped and excluded plugins - The bulk of the logic moves to a Cirrus task template. This was done instead of rewriting the script in Dart, even though it will mean more work for alternate CI support (e.g., bringing this up on a Windows LUCI bot), because breaking it into components makes it easier to pinpoint failures from the CI UI rather than having all the steps smashed together. --- .cirrus.yml | 36 ++++++--- script/build_all_plugins_app.sh | 73 ------------------- script/common.sh | 14 ---- script/configs/exclude_all_plugins_app.yaml | 10 +++ .../tool/lib/src/common/plugin_command.dart | 4 +- .../src/create_all_plugins_app_command.dart | 28 +++++-- .../create_all_plugins_app_command_test.dart | 42 +++++++++-- script/tool_runner.sh | 6 +- 8 files changed, 99 insertions(+), 114 deletions(-) delete mode 100755 script/build_all_plugins_app.sh delete mode 100644 script/common.sh create mode 100644 script/configs/exclude_all_plugins_app.yaml diff --git a/.cirrus.yml b/.cirrus.yml index f978cc729799..ffdd71daebc4 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -28,6 +28,20 @@ flutter_upgrade_template: &FLUTTER_UPGRADE_TEMPLATE - flutter doctor -v << : *TOOL_SETUP_TEMPLATE +build_all_plugins_app_template: &BUILD_ALL_PLUGINS_APP_TEMPLATE + create_all_plugins_app_script: + - dart $PLUGIN_TOOL all-plugins-app --output-dir=. --exclude script/configs/exclude_all_plugins_app.yaml + build_all_plugins_debug_script: + - cd all_plugins + - if [[ "$BUILD_ALL_ARGS" == "web" ]]; then + - echo "Skipping; web does not support debug builds" + - else + - flutter build $BUILD_ALL_ARGS --debug + - fi + build_all_plugins_release_script: + - cd all_plugins + - flutter build $BUILD_ALL_ARGS --release + macos_template: &MACOS_TEMPLATE # Only one macOS task can run in parallel without credits, so use them for # PRs on macOS. @@ -82,28 +96,29 @@ task: ### Android tasks ### - name: build_all_plugins_apk env: + BUILD_ALL_ARGS: "apk" matrix: CHANNEL: "master" CHANNEL: "stable" - script: - - ./script/build_all_plugins_app.sh apk + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE ### Web tasks ### - name: build_all_plugins_web env: + BUILD_ALL_ARGS: "web" matrix: CHANNEL: "master" CHANNEL: "stable" - script: - - ./script/build_all_plugins_app.sh web + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE ### Linux desktop tasks ### - name: build_all_plugins_linux env: + BUILD_ALL_ARGS: "linux" matrix: CHANNEL: "master" CHANNEL: "stable" - script: + setup_script: - flutter config --enable-linux-desktop - - ./script/build_all_plugins_app.sh linux + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - name: build-linux+drive-examples env: matrix: @@ -200,11 +215,11 @@ task: ### iOS tasks ### - name: build_all_plugins_ipa env: + BUILD_ALL_ARGS: "ios --no-codesign" matrix: CHANNEL: "master" CHANNEL: "stable" - script: - - ./script/build_all_plugins_app.sh ios --no-codesign + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - name: build-ipas+drive-examples env: PATH: $PATH:/usr/local/bin @@ -234,12 +249,13 @@ task: ### macOS desktop tasks ### - name: build_all_plugins_macos env: + BUILD_ALL_ARGS: "macos" matrix: CHANNEL: "master" CHANNEL: "stable" - script: + setup_script: - flutter config --enable-macos-desktop - - ./script/build_all_plugins_app.sh macos + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - name: build-macos+drive-examples env: matrix: diff --git a/script/build_all_plugins_app.sh b/script/build_all_plugins_app.sh deleted file mode 100755 index 3b3416021a42..000000000000 --- a/script/build_all_plugins_app.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash -# 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. - -# Usage: -# -# ./script/build_all_plugins_app.sh apk -# ./script/build_all_plugins_app.sh ios - -# This script builds the app in flutter/plugins/example/all_plugins to make -# sure all first party plugins can be compiled together. - -# So that users can run this script from anywhere and it will work as expected. -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null && pwd)" - -readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" - -source "$SCRIPT_DIR/common.sh" - -# This list should be kept as short as possible, and things should remain here -# only as long as necessary, since in general the goal is for all of the latest -# versions of plugins to be mutually compatible. -# -# An example use case for this list would be to temporarily add plugins while -# updating multiple plugins for a breaking change in a common dependency in -# cases where using a relaxed version constraint isn't possible. -readonly EXCLUDED_PLUGINS_LIST=( - "plugin_platform_interface" # This should never be a direct app dependency. -) -# Comma-separated string of the list above -readonly EXCLUDED=$(IFS=, ; echo "${EXCLUDED_PLUGINS_LIST[*]}") - -ALL_EXCLUDED=($EXCLUDED) - -echo "Excluding the following plugins: $ALL_EXCLUDED" - -(cd "$REPO_DIR" && plugin_tools all-plugins-app --exclude $ALL_EXCLUDED) - -# Master now creates null-safe app code by default; migrate stable so both -# branches are building in the same mode. -if [[ "${CHANNEL}" == "stable" ]]; then - (cd $REPO_DIR/all_plugins && dart migrate --apply-changes) -fi - -function error() { - echo "$@" 1>&2 -} - -failures=0 - -BUILD_MODES=("debug" "release") -# Web doesn't support --debug for builds. -if [[ "$1" == "web" ]]; then - BUILD_MODES=("release") -fi - -for version in "${BUILD_MODES[@]}"; do - echo "Building $version..." - (cd $REPO_DIR/all_plugins && flutter build $@ --$version) - - if [ $? -eq 0 ]; then - echo "Successfully built $version all_plugins app." - echo "All first-party plugins compile together." - else - error "Failed to build $version all_plugins app." - error "This indicates a conflict between two or more first-party plugins." - failures=$(($failures + 1)) - fi -done - -rm -rf $REPO_DIR/all_plugins/ -exit $failures diff --git a/script/common.sh b/script/common.sh deleted file mode 100644 index 11eb64101f2b..000000000000 --- a/script/common.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -# 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. - -function error() { - echo "$@" 1>&2 -} - -# Runs the plugin tools from the plugin_tools git submodule. -function plugin_tools() { - (pushd "$REPO_DIR/script/tool" && dart pub get && popd) >/dev/null - dart run "$REPO_DIR/script/tool/bin/flutter_plugin_tools.dart" "$@" -} diff --git a/script/configs/exclude_all_plugins_app.yaml b/script/configs/exclude_all_plugins_app.yaml new file mode 100644 index 000000000000..8dd0fde5ef5f --- /dev/null +++ b/script/configs/exclude_all_plugins_app.yaml @@ -0,0 +1,10 @@ +# This list should be kept as short as possible, and things should remain here +# only as long as necessary, since in general the goal is for all of the latest +# versions of plugins to be mutually compatible. +# +# An example use case for this list would be to temporarily add plugins while +# updating multiple plugins for a breaking change in a common dependency in +# cases where using a relaxed version constraint isn't possible. + +# This is a permament entry, as it should never be a direct app dependency. +- plugin_platform_interface diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index db0a821fd2d7..10f423360878 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -191,7 +191,7 @@ abstract class PluginCommand extends Command { } /// Returns the set of plugins to exclude based on the `--exclude` argument. - Set _getExcludedPackageName() { + Set getExcludedPackageNames() { final Set excludedPackages = _excludedPackages ?? getStringListArg(_excludeArg).expand((String item) { if (item.endsWith('.yaml')) { @@ -265,7 +265,7 @@ abstract class PluginCommand extends Command { Stream _getAllPackages() async* { Set plugins = Set.from(getStringListArg(_packagesArg)); - final Set excludedPluginNames = _getExcludedPackageName(); + final Set excludedPluginNames = getExcludedPackageNames(); final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); if (plugins.isEmpty && diff --git a/script/tool/lib/src/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_plugins_app_command.dart index d4eccb8a313e..e1cee6f3fe7d 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -12,22 +12,27 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/plugin_command.dart'; +const String _outputDirectoryFlag = 'output-dir'; + /// A command to create an application that builds all in a single application. class CreateAllPluginsAppCommand extends PluginCommand { /// Creates an instance of the builder command. CreateAllPluginsAppCommand( Directory packagesDir, { Directory? pluginsRoot, - }) : pluginsRoot = pluginsRoot ?? packagesDir.fileSystem.currentDirectory, - super(packagesDir) { - appDirectory = this.pluginsRoot.childDirectory('all_plugins'); + }) : super(packagesDir) { + final Directory defaultDir = + pluginsRoot ?? packagesDir.fileSystem.currentDirectory; + argParser.addOption(_outputDirectoryFlag, + defaultsTo: defaultDir.path, + help: 'The path the directory to create the "all_plugins" project in.\n' + 'Defaults to the repository root.'); } - /// The root directory of the plugin repository. - Directory pluginsRoot; - /// The location of the synthesized app project. - late Directory appDirectory; + Directory get appDirectory => packagesDir.fileSystem + .directory(getStringArg(_outputDirectoryFlag)) + .childDirectory('all_plugins'); @override String get description => @@ -43,6 +48,15 @@ class CreateAllPluginsAppCommand extends PluginCommand { throw ToolExit(exitCode); } + final Set excluded = getExcludedPackageNames(); + if (excluded.isNotEmpty) { + print('Exluding the following plugins from the combined build:'); + for (final String plugin in excluded) { + print(' $plugin'); + } + print(''); + } + await Future.wait(>[ _genPubspecWithAllPlugins(), _updateAppGradle(), diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index 073024a17bb3..4439d13c3625 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -13,10 +13,10 @@ import 'util.dart'; void main() { group('$CreateAllPluginsAppCommand', () { late CommandRunner runner; - FileSystem fileSystem; + late CreateAllPluginsAppCommand command; + late FileSystem fileSystem; late Directory testRoot; late Directory packagesDir; - late Directory appDir; setUp(() { // Since the core of this command is a call to 'flutter create', the test @@ -26,11 +26,10 @@ void main() { testRoot = fileSystem.systemTempDirectory.createTempSync(); packagesDir = testRoot.childDirectory('packages'); - final CreateAllPluginsAppCommand command = CreateAllPluginsAppCommand( + command = CreateAllPluginsAppCommand( packagesDir, pluginsRoot: testRoot, ); - appDir = command.appDirectory; runner = CommandRunner( 'create_all_test', 'Test for $CreateAllPluginsAppCommand'); runner.addCommand(command); @@ -47,7 +46,7 @@ void main() { await runCapturingPrint(runner, ['all-plugins-app']); final List pubspec = - appDir.childFile('pubspec.yaml').readAsLinesSync(); + command.appDirectory.childFile('pubspec.yaml').readAsLinesSync(); expect( pubspec, @@ -65,7 +64,7 @@ void main() { await runCapturingPrint(runner, ['all-plugins-app']); final List pubspec = - appDir.childFile('pubspec.yaml').readAsLinesSync(); + command.appDirectory.childFile('pubspec.yaml').readAsLinesSync(); expect( pubspec, @@ -82,9 +81,38 @@ void main() { await runCapturingPrint(runner, ['all-plugins-app']); final String pubspec = - appDir.childFile('pubspec.yaml').readAsStringSync(); + command.appDirectory.childFile('pubspec.yaml').readAsStringSync(); expect(pubspec, contains(RegExp('sdk:\\s*(?:["\']>=|[^])2\\.12\\.'))); }); + + test('handles --output-dir', () async { + createFakePlugin('plugina', packagesDir); + + final Directory customOutputDir = + fileSystem.systemTempDirectory.createTempSync(); + await runCapturingPrint(runner, + ['all-plugins-app', '--output-dir=${customOutputDir.path}']); + + expect(command.appDirectory.path, + customOutputDir.childDirectory('all_plugins').path); + }); + + test('logs exclusions', () async { + createFakePlugin('plugina', packagesDir); + createFakePlugin('pluginb', packagesDir); + createFakePlugin('pluginc', packagesDir); + + final List output = await runCapturingPrint( + runner, ['all-plugins-app', '--exclude=pluginb,pluginc']); + + expect( + output, + containsAllInOrder([ + 'Exluding the following plugins from the combined build:', + ' pluginb', + ' pluginc', + ])); + }); }); } diff --git a/script/tool_runner.sh b/script/tool_runner.sh index 11a54ce435a4..93a7776d0a35 100755 --- a/script/tool_runner.sh +++ b/script/tool_runner.sh @@ -8,7 +8,11 @@ set -e readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" -source "$SCRIPT_DIR/common.sh" +# Runs the plugin tools from the in-tree source. +function plugin_tools() { + (pushd "$REPO_DIR/script/tool" && dart pub get && popd) >/dev/null + dart run "$REPO_DIR/script/tool/bin/flutter_plugin_tools.dart" "$@" +} ACTIONS=("$@") From 3ae3a027e40df088b1a354d9f27b840791473aac Mon Sep 17 00:00:00 2001 From: Balvinder Singh Gambhir Date: Sat, 14 Aug 2021 06:32:03 +0530 Subject: [PATCH 072/123] [video_player] removed video player is not functional on ios simulators warning (#4241) --- packages/video_player/video_player/CHANGELOG.md | 4 ++++ packages/video_player/video_player/README.md | 2 -- packages/video_player/video_player/pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index bfed1615f8a6..8898ba665cf9 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.13 + +* Removed obsolete warning about not working in iOS simulators from README. + ## 2.1.12 * Update the video url in the readme code sample diff --git a/packages/video_player/video_player/README.md b/packages/video_player/video_player/README.md index a1d3d935e71c..4d2bf80a2628 100644 --- a/packages/video_player/video_player/README.md +++ b/packages/video_player/video_player/README.md @@ -12,8 +12,6 @@ First, add `video_player` as a [dependency in your pubspec.yaml file](https://fl ### iOS -Warning: The video player is not functional on iOS simulators. An iOS device must be used during development/testing. - Add the following entry to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: ```xml diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 960f0c6ce63a..c4c616cc751f 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.1.12 +version: 2.1.13 environment: sdk: ">=2.12.0 <3.0.0" From 99c5f6139a196171116de058a68166dc9d0325dc Mon Sep 17 00:00:00 2001 From: nt4f04uNd Date: Sun, 15 Aug 2021 20:32:03 +0300 Subject: [PATCH 073/123] Move test packages from `dependencies` to `dev_dependencies` (#4231) --- .../in_app_purchase/in_app_purchase_android/CHANGELOG.md | 4 ++++ .../in_app_purchase/in_app_purchase_android/pubspec.yaml | 4 ++-- packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md | 4 ++++ packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml | 4 ++-- packages/video_player/video_player/CHANGELOG.md | 4 ++++ packages/video_player/video_player/pubspec.yaml | 6 +++--- 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index 32f9aa60e4ca..d67d1efd61b5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.1.4+4 + +* Removed dependency on the `test` package. + # 0.1.4+3 - Updated installation instructions in README. diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml index f8e63821657a..3969e34c052b 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.4+3 +version: 0.1.4+4 environment: sdk: ">=2.12.0 <3.0.0" @@ -22,10 +22,10 @@ dependencies: in_app_purchase_platform_interface: ^1.1.0 json_annotation: ^4.0.1 meta: ^1.3.0 - test: ^1.16.0 dev_dependencies: build_runner: ^1.11.1 flutter_test: sdk: flutter json_serializable: ^4.1.1 + test: ^1.16.0 diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md index 305d5a13647c..c76409521e2f 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.1.3+2 + +* Removed dependency on the `test` package. + # 0.1.3+1 - Updated installation instructions in README. diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index 5f3b08520eb6..8fc42371f405 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_ios description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.3+1 +version: 0.1.3+2 environment: sdk: ">=2.12.0 <3.0.0" @@ -21,10 +21,10 @@ dependencies: in_app_purchase_platform_interface: ^1.1.0 json_annotation: ^4.0.1 meta: ^1.3.0 - test: ^1.16.0 dev_dependencies: build_runner: ^1.11.1 flutter_test: sdk: flutter json_serializable: ^4.1.1 + test: ^1.16.0 diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 8898ba665cf9..f2029622f0ee 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.14 + +* Removed dependency on the `flutter_test` package. + ## 2.1.13 * Removed obsolete warning about not working in iOS simulators from README. diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index c4c616cc751f..0d0cdb1cb436 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.1.13 +version: 2.1.14 environment: sdk: ">=2.12.0 <3.0.0" @@ -23,8 +23,6 @@ flutter: dependencies: flutter: sdk: flutter - flutter_test: - sdk: flutter meta: ^1.3.0 video_player_platform_interface: ^4.1.0 # The design on https://flutter.dev/go/federated-plugins was to leave @@ -36,5 +34,7 @@ dependencies: video_player_web: ^2.0.0 dev_dependencies: + flutter_test: + sdk: flutter pedantic: ^1.10.0 pigeon: ^0.1.21 From 954041d5bc76a9747899d0c7b3c66fc941e27c3f Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Mon, 16 Aug 2021 16:27:05 -0700 Subject: [PATCH 074/123] Add unit tests to `quick_actions` plugin (#4245) --- .../quick_actions/android/build.gradle | 9 ++++ .../quickactions/QuickActionsTest.java | 54 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java diff --git a/packages/quick_actions/quick_actions/android/build.gradle b/packages/quick_actions/quick_actions/android/build.gradle index 038f9e99048a..0bce642f3e60 100644 --- a/packages/quick_actions/quick_actions/android/build.gradle +++ b/packages/quick_actions/quick_actions/android/build.gradle @@ -32,6 +32,15 @@ android { disable 'InvalidPackage' } + dependencies { + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:3.2.4' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } testOptions { unitTests.includeAndroidResources = true diff --git a/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java b/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java new file mode 100644 index 000000000000..d437444a53be --- /dev/null +++ b/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java @@ -0,0 +1,54 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.quickactions; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.StandardMethodCodec; +import java.nio.ByteBuffer; +import org.junit.Test; + +public class QuickActionsTest { + private static class TestBinaryMessenger implements BinaryMessenger { + public MethodCall lastMethodCall; + + @Override + public void send(@NonNull String channel, @Nullable ByteBuffer message) { + send(channel, message, null); + } + + @Override + public void send( + @NonNull String channel, + @Nullable ByteBuffer message, + @Nullable final BinaryReply callback) { + if (channel.equals("plugins.flutter.io/quick_actions")) { + lastMethodCall = + StandardMethodCodec.INSTANCE.decodeMethodCall((ByteBuffer) message.position(0)); + } + } + + @Override + public void setMessageHandler(@NonNull String channel, @Nullable BinaryMessageHandler handler) { + // Do nothing. + } + } + + @Test + public void canAttachToEngine() { + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); + when(mockPluginBinding.getBinaryMessenger()).thenReturn(testBinaryMessenger); + + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + plugin.onAttachedToEngine(mockPluginBinding); + } +} From c52ae9fdf1751cb86ef301b48fab69d4fd6cf832 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 17 Aug 2021 08:36:40 -0700 Subject: [PATCH 075/123] [flutter_plugin_tool] Don't allow NEXT on version bumps (#4246) The special "NEXT" entry in a CHANGELOG should never be present in a commit that bumped the version. This validates that this is true even if the CHANGELOG would be correct for a non-version-change state, to catch someone incorrectly resolving a merge conflict by leaving both parts of the conflict, rather than folding the 'NEXT' entry's list into the new version's notes. Fixes https://github.com/flutter/flutter/issues/85584 --- script/tool/CHANGELOG.md | 2 + .../tool/lib/src/version_check_command.dart | 84 ++++++++++++++----- .../tool/test/version_check_command_test.dart | 54 +++++++++++- 3 files changed, 115 insertions(+), 25 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 7f326ff3c8f7..584ea571f0e1 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -23,6 +23,8 @@ the new `native-test` command. - Commands that print a run summary at the end now track and log exclusions similarly to skips for easier auditing. +- `version-check` now validates that `NEXT` is not present when changing + the version. ## 0.4.1 diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index c08600c3f669..67c563782888 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -32,6 +32,21 @@ enum NextVersionType { RELEASE, } +/// The state of a package's version relative to the comparison base. +enum _CurrentVersionState { + /// The version is unchanged. + unchanged, + + /// The version has changed, and the transition is valid. + validChange, + + /// The version has changed, and the transition is invalid. + invalidChange, + + /// There was an error determining the version state. + unknown, +} + /// Returns the set of allowed next versions, with their change type, for /// [version]. /// @@ -140,11 +155,28 @@ class VersionCheckCommand extends PackageLoopingCommand { final List errors = []; - if (!await _hasValidVersionChange(package, pubspec: pubspec)) { - errors.add('Disallowed version change.'); + bool versionChanged; + final _CurrentVersionState versionState = + await _getVersionState(package, pubspec: pubspec); + switch (versionState) { + case _CurrentVersionState.unchanged: + versionChanged = false; + break; + case _CurrentVersionState.validChange: + versionChanged = true; + break; + case _CurrentVersionState.invalidChange: + versionChanged = true; + errors.add('Disallowed version change.'); + break; + case _CurrentVersionState.unknown: + versionChanged = false; + errors.add('Unable to determine previous version.'); + break; } - if (!(await _hasConsistentVersion(package, pubspec: pubspec))) { + if (!(await _validateChangelogVersion(package, + pubspec: pubspec, pubspecVersionChanged: versionChanged))) { errors.add('pubspec.yaml and CHANGELOG.md have different versions'); } @@ -195,10 +227,9 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} return await gitVersionFinder.getPackageVersion(gitPath); } - /// Returns true if the version of [package] is either unchanged relative to - /// the comparison base (git or pub, depending on flags), or is a valid - /// version transition. - Future _hasValidVersionChange( + /// Returns the state of the verison of [package] relative to the comparison + /// base (git or pub, depending on flags). + Future<_CurrentVersionState> _getVersionState( Directory package, { required Pubspec pubspec, }) async { @@ -208,7 +239,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} if (getBoolArg(_againstPubFlag)) { previousVersion = await _fetchPreviousVersionFromPub(pubspec.name); if (previousVersion == null) { - return false; + return _CurrentVersionState.unknown; } if (previousVersion != Version.none) { print( @@ -225,12 +256,12 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} '${getBoolArg(_againstPubFlag) ? 'on pub server' : 'at git base'}.'); logWarning( '${indentation}If this plugin is not new, something has gone wrong.'); - return true; + return _CurrentVersionState.validChange; // Assume new, thus valid. } if (previousVersion == currentVersion) { print('${indentation}No version change.'); - return true; + return _CurrentVersionState.unchanged; } // Check for reverts when doing local validation. @@ -241,9 +272,9 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} // to be a revert rather than a typo by checking that the transition // from the lower version to the new version would have been valid. if (possibleVersionsFromNewVersion.containsKey(previousVersion)) { - print('${indentation}New version is lower than previous version. ' + logWarning('${indentation}New version is lower than previous version. ' 'This is assumed to be a revert.'); - return true; + return _CurrentVersionState.validChange; } } @@ -257,7 +288,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} printError('${indentation}Incorrectly updated version.\n' '${indentation}HEAD: $currentVersion, $source: $previousVersion.\n' '${indentation}Allowed versions: $allowedNextVersions'); - return false; + return _CurrentVersionState.invalidChange; } final bool isPlatformInterface = @@ -268,16 +299,20 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} allowedNextVersions[currentVersion] == NextVersionType.BREAKING_MAJOR) { printError('${indentation}Breaking change detected.\n' '${indentation}Breaking changes to platform interfaces are strongly discouraged.\n'); - return false; + return _CurrentVersionState.invalidChange; } - return true; + return _CurrentVersionState.validChange; } - /// Returns whether or not the pubspec version and CHANGELOG version for - /// [plugin] match. - Future _hasConsistentVersion( + /// Checks whether or not [package]'s CHANGELOG's versioning is correct, + /// both that it matches [pubspec] and that NEXT is used correctly, printing + /// the results of its checks. + /// + /// Returns false if the CHANGELOG fails validation. + Future _validateChangelogVersion( Directory package, { required Pubspec pubspec, + required bool pubspecVersionChanged, }) async { // This method isn't called unless `version` is non-null. final Version fromPubspec = pubspec.version!; @@ -296,10 +331,19 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} // Remove all leading mark down syntax from the version line. String? versionString = firstLineWithText?.split(' ').last; + final String badNextErrorMessage = '${indentation}When bumping the version ' + 'for release, the NEXT section should be incorporated into the new ' + 'version\'s release notes.'; + // Skip validation for the special NEXT version that's used to accumulate // changes that don't warrant publishing on their own. final bool hasNextSection = versionString == 'NEXT'; if (hasNextSection) { + // NEXT should not be present in a commit that changes the version. + if (pubspecVersionChanged) { + printError(badNextErrorMessage); + return false; + } print( '${indentation}Found NEXT; validating next version in the CHANGELOG.'); // Ensure that the version in pubspec hasn't changed without updating @@ -334,9 +378,7 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. if (!hasNextSection) { final RegExp nextRegex = RegExp(r'^#+\s*NEXT\s*$'); if (lines.any((String line) => nextRegex.hasMatch(line))) { - printError('${indentation}When bumping the version for release, the ' - 'NEXT section should be incorporated into the new version\'s ' - 'release notes.'); + printError(badNextErrorMessage); return false; } } diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 587de1a58cd9..7765073feb08 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -373,6 +373,10 @@ void main() { * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=master']); await expectLater( @@ -384,8 +388,7 @@ void main() { ); }); - test('Fail if NEXT is left in the CHANGELOG when adding a version bump', - () async { + test('Fail if NEXT appears after a version', () async { const String version = '1.0.1'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, version: version); @@ -419,6 +422,45 @@ void main() { ); }); + test('Fail if NEXT is left in the CHANGELOG when adding a version bump', + () async { + const String version = '1.0.1'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: version); + + const String changelog = ''' +## NEXT +* Some changes that should have been folded in 1.0.1. +## $version +* Some changes. +## 1.0.0 +* Some other changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + + bool hasError = false; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--against-pub' + ], errorHandler: (Error e) { + expect(e, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + expect( + output, + containsAllInOrder([ + contains('When bumping the version for release, the NEXT section ' + 'should be incorporated into the new version\'s release notes.') + ]), + ); + }); + test('Fail if the version changes without replacing NEXT', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, version: '1.0.1'); @@ -430,6 +472,10 @@ void main() { * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + bool hasError = false; final List output = await runCapturingPrint(runner, [ 'version-check', @@ -444,8 +490,8 @@ void main() { expect( output, containsAllInOrder([ - contains('Found NEXT; validating next version in the CHANGELOG.'), - contains('Versions in CHANGELOG.md and pubspec.yaml do not match.'), + contains('When bumping the version for release, the NEXT section ' + 'should be incorporated into the new version\'s release notes.') ]), ); }); From d58036f45d825ffbf311ab48a53d993d40468d5a Mon Sep 17 00:00:00 2001 From: Monika Manuela Hengki Date: Wed, 18 Aug 2021 00:20:08 +0800 Subject: [PATCH 076/123] [quick_actions] Android support only calling initialize once (#4204) Fixes flutter/flutter#87259 --- .../quick_actions/quick_actions/CHANGELOG.md | 4 + .../quickactions/MethodCallHandlerImpl.java | 3 +- .../quickactions/QuickActionsPlugin.java | 21 +++- .../quickactions/QuickActionsTest.java | 111 ++++++++++++++++++ .../quick_actions/lib/quick_actions.dart | 2 +- .../quick_actions/quick_actions/pubspec.yaml | 2 +- .../quick_actions_platform.dart | 2 +- 7 files changed, 139 insertions(+), 6 deletions(-) diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md index 4f8943845cf7..5d040f4fd74e 100644 --- a/packages/quick_actions/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.0+5 + +* Support only calling initialize once. + ## 0.6.0+4 * Remove references to the Android V1 embedding. diff --git a/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java b/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java index 465283053442..2d89352f3e09 100644 --- a/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java +++ b/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java @@ -20,9 +20,8 @@ import java.util.Map; class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { - + protected static final String EXTRA_ACTION = "some unique action key"; private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; - private static final String EXTRA_ACTION = "some unique action key"; private final Context context; private Activity activity; diff --git a/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java b/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java index ab3431325503..b2f80ad0a271 100644 --- a/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java +++ b/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java @@ -6,14 +6,17 @@ import android.app.Activity; import android.content.Context; +import android.content.Intent; +import android.os.Build; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry.NewIntentListener; /** QuickActionsPlugin */ -public class QuickActionsPlugin implements FlutterPlugin, ActivityAware { +public class QuickActionsPlugin implements FlutterPlugin, ActivityAware, NewIntentListener { private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; private MethodChannel channel; @@ -43,6 +46,8 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) { @Override public void onAttachedToActivity(ActivityPluginBinding binding) { handler.setActivity(binding.getActivity()); + binding.addOnNewIntentListener(this); + onNewIntent(binding.getActivity().getIntent()); } @Override @@ -52,6 +57,7 @@ public void onDetachedFromActivity() { @Override public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + binding.removeOnNewIntentListener(this); onAttachedToActivity(binding); } @@ -60,6 +66,19 @@ public void onDetachedFromActivityForConfigChanges() { onDetachedFromActivity(); } + @Override + public boolean onNewIntent(Intent intent) { + // Do nothing for anything lower than API 25 as the functionality isn't supported. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + return false; + } + // Notify the Dart side if the launch intent has the intent extra relevant to quick actions. + if (intent.hasExtra(MethodCallHandlerImpl.EXTRA_ACTION) && channel != null) { + channel.invokeMethod("launch", intent.getStringExtra(MethodCallHandlerImpl.EXTRA_ACTION)); + } + return false; + } + private void setupChannel(BinaryMessenger messenger, Context context, Activity activity) { channel = new MethodChannel(messenger, CHANNEL_ID); handler = new MethodCallHandlerImpl(context, activity); diff --git a/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java b/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java index d437444a53be..208a119efafe 100644 --- a/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java +++ b/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java @@ -4,17 +4,30 @@ package io.flutter.plugins.quickactions; +import static io.flutter.plugins.quickactions.MethodCallHandlerImpl.EXTRA_ACTION; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.app.Activity; +import android.content.Intent; +import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.StandardMethodCodec; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.nio.ByteBuffer; +import org.junit.After; import org.junit.Test; +import org.mockito.internal.util.reflection.FieldSetter; public class QuickActionsTest { private static class TestBinaryMessenger implements BinaryMessenger { @@ -42,6 +55,10 @@ public void setMessageHandler(@NonNull String channel, @Nullable BinaryMessageHa } } + static final int SUPPORTED_BUILD = 25; + static final int UNSUPPORTED_BUILD = 24; + static final String SHORTCUT_TYPE = "action_one"; + @Test public void canAttachToEngine() { final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); @@ -51,4 +68,98 @@ public void canAttachToEngine() { final QuickActionsPlugin plugin = new QuickActionsPlugin(); plugin.onAttachedToEngine(mockPluginBinding); } + + @Test + public void onAttachedToActivity_buildVersionSupported_invokesLaunchMethod() + throws NoSuchFieldException, IllegalAccessException { + // Arrange + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); + setBuildVersion(SUPPORTED_BUILD); + FieldSetter.setField( + plugin, + QuickActionsPlugin.class.getDeclaredField("handler"), + mock(MethodCallHandlerImpl.class)); + final Intent mockIntent = createMockIntentWithQuickActionExtra(); + final Activity mockMainActivity = mock(Activity.class); + when(mockMainActivity.getIntent()).thenReturn(mockIntent); + final ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class); + when(mockActivityPluginBinding.getActivity()).thenReturn(mockMainActivity); + + // Act + plugin.onAttachedToActivity(mockActivityPluginBinding); + + // Assert + assertNotNull(testBinaryMessenger.lastMethodCall); + assertEquals(testBinaryMessenger.lastMethodCall.method, "launch"); + assertEquals(testBinaryMessenger.lastMethodCall.arguments, SHORTCUT_TYPE); + } + + @Test + public void onNewIntent_buildVersionUnsupported_doesNotInvokeMethod() + throws NoSuchFieldException, IllegalAccessException { + // Arrange + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); + setBuildVersion(UNSUPPORTED_BUILD); + final Intent mockIntent = createMockIntentWithQuickActionExtra(); + + // Act + final boolean onNewIntentReturn = plugin.onNewIntent(mockIntent); + + // Assert + assertNull(testBinaryMessenger.lastMethodCall); + assertFalse(onNewIntentReturn); + } + + @Test + public void onNewIntent_buildVersionSupported_invokesLaunchMethod() + throws NoSuchFieldException, IllegalAccessException { + // Arrange + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); + setBuildVersion(SUPPORTED_BUILD); + final Intent mockIntent = createMockIntentWithQuickActionExtra(); + + // Act + final boolean onNewIntentReturn = plugin.onNewIntent(mockIntent); + + // Assert + assertNotNull(testBinaryMessenger.lastMethodCall); + assertEquals(testBinaryMessenger.lastMethodCall.method, "launch"); + assertEquals(testBinaryMessenger.lastMethodCall.arguments, SHORTCUT_TYPE); + assertFalse(onNewIntentReturn); + } + + private void setUpMessengerAndFlutterPluginBinding( + TestBinaryMessenger testBinaryMessenger, QuickActionsPlugin plugin) { + final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); + when(mockPluginBinding.getBinaryMessenger()).thenReturn(testBinaryMessenger); + plugin.onAttachedToEngine(mockPluginBinding); + } + + private Intent createMockIntentWithQuickActionExtra() { + final Intent mockIntent = mock(Intent.class); + when(mockIntent.hasExtra(EXTRA_ACTION)).thenReturn(true); + when(mockIntent.getStringExtra(EXTRA_ACTION)).thenReturn(QuickActionsTest.SHORTCUT_TYPE); + return mockIntent; + } + + private void setBuildVersion(int buildVersion) + throws NoSuchFieldException, IllegalAccessException { + Field buildSdkField = Build.VERSION.class.getField("SDK_INT"); + buildSdkField.setAccessible(true); + final Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(buildSdkField, buildSdkField.getModifiers() & ~Modifier.FINAL); + buildSdkField.set(null, buildVersion); + } + + @After + public void tearDown() throws NoSuchFieldException, IllegalAccessException { + setBuildVersion(0); + } } diff --git a/packages/quick_actions/quick_actions/lib/quick_actions.dart b/packages/quick_actions/quick_actions/lib/quick_actions.dart index 6907f25729ab..7d3d4ad1ef3b 100644 --- a/packages/quick_actions/quick_actions/lib/quick_actions.dart +++ b/packages/quick_actions/quick_actions/lib/quick_actions.dart @@ -16,7 +16,7 @@ class QuickActions { /// Initializes this plugin. /// - /// Call this once before any further interaction with the the plugin. + /// Call this once before any further interaction with the plugin. Future initialize(QuickActionHandler handler) async => QuickActionsPlatform.instance.initialize(handler); diff --git a/packages/quick_actions/quick_actions/pubspec.yaml b/packages/quick_actions/quick_actions/pubspec.yaml index 657c2f001a83..e52ab515432f 100644 --- a/packages/quick_actions/quick_actions/pubspec.yaml +++ b/packages/quick_actions/quick_actions/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for creating shortcuts on home screen, also known as Quick Actions on iOS and App Shortcuts on Android. repository: https://github.com/flutter/plugins/tree/master/packages/quick_actions issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+quick_actions%22 -version: 0.6.0+4 +version: 0.6.0+5 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/quick_actions/quick_actions_platform_interface/lib/platform_interface/quick_actions_platform.dart b/packages/quick_actions/quick_actions_platform_interface/lib/platform_interface/quick_actions_platform.dart index b15fb8b43233..2e06935ccb09 100644 --- a/packages/quick_actions/quick_actions_platform_interface/lib/platform_interface/quick_actions_platform.dart +++ b/packages/quick_actions/quick_actions_platform_interface/lib/platform_interface/quick_actions_platform.dart @@ -38,7 +38,7 @@ abstract class QuickActionsPlatform extends PlatformInterface { /// Initializes this plugin. /// - /// Call this once before any further interaction with the the plugin. + /// Call this once before any further interaction with the plugin. Future initialize(QuickActionHandler handler) async { throw UnimplementedError("initialize() has not been implemented."); } From 04ea39acd6d8c2ec0ed9bb022ae225470285060a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 17 Aug 2021 09:43:31 -0700 Subject: [PATCH 077/123] [flutter_plugin_tools] Add Android native UI test support (#4188) Adds integration test support for Android to `native-test`. Also fixes an issue where the existing unit test support was not honoring `--no-unit`. Fixes https://github.com/flutter/flutter/issues/86490 --- .../java/io/plugins/DartIntegrationTest.java | 14 ++ .../androidalarmmanager/MainActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../MainActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../plugins/battery/FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../cameraexample/FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../MainActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../googlemapsexample/MainActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../FlutterFragmentActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../packageinfoexample/MainActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../pathprovider}/MainActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../sensorsexample/FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../shareexample/FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../urllauncherexample/MainActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../MainActivityTest.java | 2 + script/tool/CHANGELOG.md | 4 + script/tool/lib/src/native_test_command.dart | 124 +++++++--- script/tool/pubspec.yaml | 2 +- .../tool/test/native_test_command_test.dart | 227 +++++++++++++++++- 42 files changed, 623 insertions(+), 38 deletions(-) create mode 100644 packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/DartIntegrationTest.java create mode 100644 packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java rename packages/path_provider/path_provider/example/android/app/src/androidTest/java/{ => io/flutter/plugins/pathprovider}/MainActivityTest.java (89%) create mode 100644 packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java diff --git a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/DartIntegrationTest.java b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java index 0272c14a8328..a5bb72415f14 100644 --- a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java +++ b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule diff --git a/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java b/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/MainActivityTest.java b/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/MainActivityTest.java index d9ba10729001..358fc78bfcfd 100644 --- a/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/MainActivityTest.java +++ b/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/MainActivityTest.java @@ -6,9 +6,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); diff --git a/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java index 267271f70f42..5068d043bdfc 100644 --- a/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java +++ b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java b/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java b/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java index 32acc1ba9c15..39cae489d9fa 100644 --- a/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java +++ b/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/FlutterActivityTest.java b/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/FlutterActivityTest.java index 330f0050a1d8..b4a67622f8dc 100644 --- a/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/FlutterActivityTest.java +++ b/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/MainActivityTest.java b/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/MainActivityTest.java index 66a606ca00a9..25999995691d 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/MainActivityTest.java +++ b/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/MainActivityTest.java @@ -6,9 +6,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java index fccd4c95c3ac..244a22b6c6c8 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java index 36787ffd9910..edc01de491af 100644 --- a/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java index 1ca37ce5feb7..91e068fa8043 100644 --- a/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java +++ b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java index a60599573d57..03e4066de85e 100644 --- a/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java +++ b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java index a60599573d57..03e4066de85e 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java index e5ece3edd50d..68c22371d7dd 100644 --- a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java +++ b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterFragmentActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterFragmentActivityTest { @Rule diff --git a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/MainActivityTest.java b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/MainActivityTest.java index cf7252ce19de..fb63f6f8c88b 100644 --- a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/MainActivityTest.java +++ b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/MainActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule diff --git a/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/path_provider/path_provider/example/android/app/src/androidTest/java/MainActivityTest.java b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/MainActivityTest.java similarity index 89% rename from packages/path_provider/path_provider/example/android/app/src/androidTest/java/MainActivityTest.java rename to packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/MainActivityTest.java index 0380a4397ae6..d56458bd753c 100644 --- a/packages/path_provider/path_provider/example/android/app/src/androidTest/java/MainActivityTest.java +++ b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/MainActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule diff --git a/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java index 0b60dfa53e1f..e96548da291a 100644 --- a/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java +++ b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/FlutterActivityTest.java b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/FlutterActivityTest.java index c1584aab107c..52a6b8bebaf3 100644 --- a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/FlutterActivityTest.java +++ b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/FlutterActivityTest.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/FlutterActivityTest.java index 070749dcff20..aba658887d88 100644 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/FlutterActivityTest.java +++ b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java b/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/MainActivityTest.java b/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/MainActivityTest.java index 9e343b82a193..67f15efb10aa 100644 --- a/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/MainActivityTest.java +++ b/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/MainActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java index b18308ab2feb..a32aaebb0ecd 100644 --- a/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java +++ b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 584ea571f0e1..267019fe7359 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,9 @@ ## NEXT +- Added Android native integration test support to `native-test`. + +## 0.5.0 + - `--exclude` and `--custom-analysis` now accept paths to YAML files that contain lists of packages to exclude, in addition to just package names, so that exclude lists can be maintained separately from scripts and CI diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 36b12741f2ce..9fc6a2912ccc 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -96,11 +96,6 @@ this command. throw ToolExit(exitInvalidArguments); } - if (getBoolArg(kPlatformAndroid) && getBoolArg(_integrationTestFlag)) { - logWarning('This command currently only supports unit tests for Android. ' - 'See https://github.com/flutter/flutter/issues/86490.'); - } - // iOS-specific run-level state. if (_requestedPlatforms.contains('ios')) { String destination = getStringArg(_iosDestinationFlag); @@ -178,12 +173,8 @@ this command. } Future<_PlatformResult> _testAndroid(Directory plugin, _TestMode mode) async { - final List examplesWithTests = []; - for (final Directory example in getExamplesForPlugin(plugin)) { - if (!isFlutterPackage(example)) { - continue; - } - if (example + bool exampleHasUnitTests(Directory example) { + return example .childDirectory('android') .childDirectory('app') .childDirectory('src') @@ -193,20 +184,62 @@ this command. .childDirectory('android') .childDirectory('src') .childDirectory('test') - .existsSync()) { - examplesWithTests.add(example); - } else { - _printNoExampleTestsMessage(example, 'Android'); - } + .existsSync(); } - if (examplesWithTests.isEmpty) { - return _PlatformResult(RunState.skipped); + bool exampleHasNativeIntegrationTests(Directory example) { + final Directory integrationTestDirectory = example + .childDirectory('android') + .childDirectory('app') + .childDirectory('src') + .childDirectory('androidTest'); + // There are two types of integration tests that can be in the androidTest + // directory: + // - FlutterTestRunner.class tests, which bridge to Dart integration tests + // - Purely native tests + // Only the latter is supported by this command; the former will hang if + // run here because they will wait for a Dart call that will never come. + // + // This repository uses a convention of putting the former in a + // *ActivityTest.java file, so ignore that file when checking for tests. + // Also ignore DartIntegrationTest.java, which defines the annotation used + // below for filtering the former out when running tests. + // + // If those are the only files, then there are no tests to run here. + return integrationTestDirectory.existsSync() && + integrationTestDirectory + .listSync(recursive: true) + .whereType() + .any((File file) { + final String basename = file.basename; + return !basename.endsWith('ActivityTest.java') && + basename != 'DartIntegrationTest.java'; + }); } + final Iterable examples = getExamplesForPlugin(plugin); + + bool ranTests = false; bool failed = false; bool hasMissingBuild = false; - for (final Directory example in examplesWithTests) { + for (final Directory example in examples) { + final bool hasUnitTests = exampleHasUnitTests(example); + final bool hasIntegrationTests = + exampleHasNativeIntegrationTests(example); + + if (mode.unit && !hasUnitTests) { + _printNoExampleTestsMessage(example, 'Android unit'); + } + if (mode.integration && !hasIntegrationTests) { + _printNoExampleTestsMessage(example, 'Android integration'); + } + + final bool runUnitTests = mode.unit && hasUnitTests; + final bool runIntegrationTests = mode.integration && hasIntegrationTests; + if (!runUnitTests && !runIntegrationTests) { + continue; + } + final String exampleName = getPackageDescription(example); _printRunningExampleTestsMessage(example, 'Android'); @@ -221,17 +254,52 @@ this command. continue; } - final int exitCode = await processRunner.runAndStream( - gradleFile.path, ['testDebugUnitTest'], - workingDir: androidDirectory); - if (exitCode != 0) { - printError('$exampleName tests failed.'); - failed = true; + if (runUnitTests) { + print('Running unit tests...'); + final int exitCode = await processRunner.runAndStream( + gradleFile.path, ['testDebugUnitTest'], + workingDir: androidDirectory); + if (exitCode != 0) { + printError('$exampleName unit tests failed.'); + failed = true; + } + ranTests = true; } + + if (runIntegrationTests) { + // FlutterTestRunner-based tests will hang forever if run in a normal + // app build, since they wait for a Dart call from integration_test that + // will never come. Those tests have an extra annotation to allow + // filtering them out. + const String filter = + 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; + + print('Running integration tests...'); + final int exitCode = await processRunner.runAndStream( + gradleFile.path, + [ + 'app:connectedAndroidTest', + '-Pandroid.testInstrumentationRunnerArguments.$filter', + ], + workingDir: androidDirectory); + if (exitCode != 0) { + printError('$exampleName integration tests failed.'); + failed = true; + } + ranTests = true; + } + } + + if (failed) { + return _PlatformResult(RunState.failed, + error: hasMissingBuild + ? 'Examples must be built before testing.' + : null); + } + if (!ranTests) { + return _PlatformResult(RunState.skipped); } - return _PlatformResult(failed ? RunState.failed : RunState.succeeded, - error: - hasMissingBuild ? 'Examples must be built before testing.' : null); + return _PlatformResult(RunState.succeeded); } Future<_PlatformResult> _testIos(Directory plugin, _TestMode mode) { diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 7b2cdd4f4101..02b3ca624b96 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.4.1 +version: 0.5.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index e656e2f23721..59ca17b25c0b 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -16,6 +16,10 @@ import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; +const String _androidIntegrationTestFilter = + '-Pandroid.testInstrumentationRunnerArguments.' + 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; + final Map _kDeviceListMap = { 'runtimes': >[ { @@ -353,7 +357,7 @@ void main() { }); group('Android', () { - test('runs Java tests in Android implementation folder', () async { + test('runs Java unit tests in Android implementation folder', () async { final Directory plugin = createFakePlugin( 'plugin', packagesDir, @@ -383,7 +387,7 @@ void main() { ); }); - test('runs Java tests in example folder', () async { + test('runs Java unit tests in example folder', () async { final Directory plugin = createFakePlugin( 'plugin', packagesDir, @@ -413,6 +417,172 @@ void main() { ); }); + test('runs Java integration tests', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:connectedAndroidTest', + _androidIntegrationTestFilter, + ], + androidFolder.path, + ), + ]), + ); + }); + + test( + 'ignores Java integration test files associated with integration_test', + () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java', + 'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/FlutterActivityTest.java', + 'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/MainActivityTest.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + // Nothing should run since those files are all + // integration_test-specific. + expect( + processRunner.recordedCalls, + orderedEquals([]), + ); + }); + + test('runs all tests when present', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], + androidFolder.path, + ), + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:connectedAndroidTest', + _androidIntegrationTestFilter, + ], + androidFolder.path, + ), + ]), + ); + }); + + test('honors --no-unit', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint( + runner, ['native-test', '--android', '--no-unit']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:connectedAndroidTest', + _androidIntegrationTestFilter, + ], + androidFolder.path, + ), + ]), + ); + }); + + test('honors --no-integration', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint( + runner, ['native-test', '--android', '--no-integration']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], + androidFolder.path, + ), + ]), + ); + }); + test('fails when the app needs to be built', () async { createFakePlugin( 'plugin', @@ -444,6 +614,46 @@ void main() { ); }); + test('logs missing test types', () async { + // No unit tests. + createFakePlugin( + 'plugin1', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + // No integration tests. + createFakePlugin( + 'plugin2', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + ], + ); + + final List output = await runCapturingPrint( + runner, ['native-test', '--android']); + + expect( + output, + containsAllInOrder([ + contains('No Android unit tests found for plugin1/example'), + contains('Running integration tests...'), + contains( + 'No Android integration tests found for plugin2/example'), + contains('Running unit tests...'), + ])); + }); + test('fails when a test fails', () async { final Directory pluginDir = createFakePlugin( 'plugin', @@ -478,7 +688,7 @@ void main() { expect( output, containsAllInOrder([ - contains('plugin/example tests failed.'), + contains('plugin/example unit tests failed.'), contains('The following packages had errors:'), contains('plugin') ]), @@ -518,7 +728,8 @@ void main() { expect( output, containsAllInOrder([ - contains('No Android tests found for plugin/example'), + contains('No Android unit tests found for plugin/example'), + contains('No Android integration tests found for plugin/example'), contains('SKIPPING: No tests found.'), ]), ); @@ -810,10 +1021,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], - androidFolder.path), + ProcessCall(androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], androidFolder.path), ProcessCall( 'xcrun', const [ @@ -1003,7 +1212,7 @@ void main() { output, containsAllInOrder([ contains('Running tests for Android...'), - contains('plugin/example tests failed.'), + contains('plugin/example unit tests failed.'), contains('Running tests for iOS...'), contains('Successfully ran iOS xctest for plugin/example'), contains('The following packages had errors:'), From 90fd90ed62571fb765df8c49000b14e3c8c643ec Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 17 Aug 2021 12:09:03 -0700 Subject: [PATCH 078/123] [url_launcher] Add native unit tests for Windows (#4156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a unit test target based on googletest. This is intended to be both a set of unit tests for this plugin, and also a model of changes that can be made to the `flutter create` template for Windows plugins to include better testing out of the box (https://github.com/flutter/flutter/issues/82458). In addition to the test binary being directly runnable, the integration between CMake, VS, and googletest means that these tests are visible—and runnable—in the VS Test Explorer UI after opening the generated .sln file. Changes for testing in general: - Moved the plugin class declaration to a header. - Moved the C registration API implementation to a separate file. - Added (opt-in, so it won't affect plugin client builds) plugin CMake rules to download googletest and build a new executable target that builds all the plugin sources, plus gtest and gmock. - Added a line to the example app CMake rules to enable the unit tests. - Added a unit test file. url_launcher-specific changes: - Wrapped all Win32 calls in a thin class for mockability in unit tests. - Factored some logic into helpers for better maintainability while I was refactoring anyway. Note: This unit test is not yet being run by CI. A tools command to run Windows plugin unit tests will be a separate PR. Part of https://github.com/flutter/flutter/issues/82445 --- .../url_launcher_windows/CHANGELOG.md | 4 + .../example/windows/CMakeLists.txt | 3 + .../example/windows/flutter/CMakeLists.txt | 1 + .../flutter/generated_plugin_registrant.cc | 6 +- .../url_launcher_windows/pubspec.yaml | 2 +- .../windows/CMakeLists.txt | 55 +++++- ...uncher_plugin.h => url_launcher_windows.h} | 2 +- .../windows/system_apis.cpp | 38 ++++ .../windows/system_apis.h | 56 ++++++ .../test/url_launcher_windows_test.cpp | 162 ++++++++++++++++++ .../windows/url_launcher_plugin.cpp | 97 ++++++----- .../windows/url_launcher_plugin.h | 48 ++++++ .../windows/url_launcher_windows.cpp | 15 ++ 13 files changed, 436 insertions(+), 53 deletions(-) rename packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/{url_launcher_plugin.h => url_launcher_windows.h} (92%) create mode 100644 packages/url_launcher/url_launcher_windows/windows/system_apis.cpp create mode 100644 packages/url_launcher/url_launcher_windows/windows/system_apis.h create mode 100644 packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp create mode 100644 packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h create mode 100644 packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md index d26fe19c359e..d095a52341b5 100644 --- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Added unit tests. + ## 2.0.2 * Replaced reference to `shared_preferences` plugin with the `url_launcher` in the README. diff --git a/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt index abf90408efb4..5b1622bcb333 100644 --- a/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt @@ -46,6 +46,9 @@ add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build add_subdirectory("runner") +# Enable the test target. +set(include_url_launcher_windows_tests TRUE) + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) diff --git a/packages/url_launcher/url_launcher_windows/example/windows/flutter/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/example/windows/flutter/CMakeLists.txt index c7a8c7607d81..744f08a9389b 100644 --- a/packages/url_launcher/url_launcher_windows/example/windows/flutter/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_windows/example/windows/flutter/CMakeLists.txt @@ -91,6 +91,7 @@ add_custom_command( ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" windows-x64 $ + VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" diff --git a/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc index d9fdd53925c5..4f7884874da7 100644 --- a/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc +++ b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,9 @@ #include "generated_plugin_registrant.h" -#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { - UrlLauncherPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml index 6435eda4564a..a92e91ee4568 100644 --- a/packages/url_launcher/url_launcher_windows/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml @@ -13,7 +13,7 @@ flutter: implements: url_launcher platforms: windows: - pluginClass: UrlLauncherPlugin + pluginClass: UrlLauncherWindows dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt index 57d87e3f6f85..a4185acff6a1 100644 --- a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt @@ -4,12 +4,20 @@ project(${PROJECT_NAME} LANGUAGES CXX) set(PLUGIN_NAME "${PROJECT_NAME}_plugin") -add_library(${PLUGIN_NAME} SHARED +list(APPEND PLUGIN_SOURCES + "system_apis.cpp" + "system_apis.h" "url_launcher_plugin.cpp" + "url_launcher_plugin.h" +) + +add_library(${PLUGIN_NAME} SHARED + "include/url_launcher_windows/url_launcher_windows.h" + "url_launcher_windows.cpp" + ${PLUGIN_SOURCES} ) apply_standard_settings(${PLUGIN_NAME}) -set_target_properties(${PLUGIN_NAME} PROPERTIES - CXX_VISIBILITY_PRESET hidden) +set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") @@ -20,3 +28,44 @@ set(file_chooser_bundled_libraries "" PARENT_SCOPE ) + + +# === Tests === + +if (${include_${PROJECT_NAME}_tests}) +set(TEST_RUNNER "${PROJECT_NAME}_test") +enable_testing() +# TODO(stuartmorgan): Consider using a single shared, pre-checked-in googletest +# instance rather than downloading for each plugin. This approach makes sense +# for a template, but not for a monorepo with many plugins. +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/release-1.11.0.zip +) +# Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +# Disable install commands for gtest so it doesn't end up in the bundle. +set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) + +FetchContent_MakeAvailable(googletest) + +# The plugin's C API is not very useful for unit testing, so build the sources +# directly into the test binary rather than using the DLL. +add_executable(${TEST_RUNNER} + test/url_launcher_windows_test.cpp + ${PLUGIN_SOURCES} +) +apply_standard_settings(${TEST_RUNNER}) +target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin) +target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) +# flutter_wrapper_plugin has link dependencies on the Flutter DLL. +add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${FLUTTER_LIBRARY}" $ +) + +include(GoogleTest) +gtest_discover_tests(${TEST_RUNNER}) +endif() diff --git a/packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_plugin.h b/packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_windows.h similarity index 92% rename from packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_plugin.h rename to packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_windows.h index 8af3924ded81..251471c9fe56 100644 --- a/packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_plugin.h +++ b/packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_windows.h @@ -16,7 +16,7 @@ extern "C" { #endif -FLUTTER_PLUGIN_EXPORT void UrlLauncherPluginRegisterWithRegistrar( +FLUTTER_PLUGIN_EXPORT void UrlLauncherWindowsRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar); #if defined(__cplusplus) diff --git a/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp b/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp new file mode 100644 index 000000000000..abd690b6e47f --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp @@ -0,0 +1,38 @@ +// 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. +#include "system_apis.h" + +#include + +namespace url_launcher_plugin { + +SystemApis::SystemApis() {} + +SystemApis::~SystemApis() {} + +SystemApisImpl::SystemApisImpl() {} + +SystemApisImpl::~SystemApisImpl() {} + +LSTATUS SystemApisImpl::RegCloseKey(HKEY key) { return ::RegCloseKey(key); } + +LSTATUS SystemApisImpl::RegOpenKeyExW(HKEY key, LPCWSTR sub_key, DWORD options, + REGSAM desired, PHKEY result) { + return ::RegOpenKeyExW(key, sub_key, options, desired, result); +} + +LSTATUS SystemApisImpl::RegQueryValueExW(HKEY key, LPCWSTR value_name, + LPDWORD type, LPBYTE data, + LPDWORD data_size) { + return ::RegQueryValueExW(key, value_name, nullptr, type, data, data_size); +} + +HINSTANCE SystemApisImpl::ShellExecuteW(HWND hwnd, LPCWSTR operation, + LPCWSTR file, LPCWSTR parameters, + LPCWSTR directory, int show_flags) { + return ::ShellExecuteW(hwnd, operation, file, parameters, directory, + show_flags); +} + +} // namespace url_launcher_plugin diff --git a/packages/url_launcher/url_launcher_windows/windows/system_apis.h b/packages/url_launcher/url_launcher_windows/windows/system_apis.h new file mode 100644 index 000000000000..7b56704d8e04 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/system_apis.h @@ -0,0 +1,56 @@ +// 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. +#include + +namespace url_launcher_plugin { + +// An interface wrapping system APIs used by the plugin, for mocking. +class SystemApis { + public: + SystemApis(); + virtual ~SystemApis(); + + // Disallow copy and move. + SystemApis(const SystemApis&) = delete; + SystemApis& operator=(const SystemApis&) = delete; + + // Wrapper for RegCloseKey. + virtual LSTATUS RegCloseKey(HKEY key) = 0; + + // Wrapper for RegQueryValueEx. + virtual LSTATUS RegQueryValueExW(HKEY key, LPCWSTR value_name, LPDWORD type, + LPBYTE data, LPDWORD data_size) = 0; + + // Wrapper for RegOpenKeyEx. + virtual LSTATUS RegOpenKeyExW(HKEY key, LPCWSTR sub_key, DWORD options, + REGSAM desired, PHKEY result) = 0; + + // Wrapper for ShellExecute. + virtual HINSTANCE ShellExecuteW(HWND hwnd, LPCWSTR operation, LPCWSTR file, + LPCWSTR parameters, LPCWSTR directory, + int show_flags) = 0; +}; + +// Implementation of SystemApis using the Win32 APIs. +class SystemApisImpl : public SystemApis { + public: + SystemApisImpl(); + virtual ~SystemApisImpl(); + + // Disallow copy and move. + SystemApisImpl(const SystemApisImpl&) = delete; + SystemApisImpl& operator=(const SystemApisImpl&) = delete; + + // SystemApis Implementation: + virtual LSTATUS RegCloseKey(HKEY key); + virtual LSTATUS RegOpenKeyExW(HKEY key, LPCWSTR sub_key, DWORD options, + REGSAM desired, PHKEY result); + virtual LSTATUS RegQueryValueExW(HKEY key, LPCWSTR value_name, LPDWORD type, + LPBYTE data, LPDWORD data_size); + virtual HINSTANCE ShellExecuteW(HWND hwnd, LPCWSTR operation, LPCWSTR file, + LPCWSTR parameters, LPCWSTR directory, + int show_flags); +}; + +} // namespace url_launcher_plugin diff --git a/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp new file mode 100644 index 000000000000..191d51a0caa8 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp @@ -0,0 +1,162 @@ +// 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. +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "url_launcher_plugin.h" + +namespace url_launcher_plugin { +namespace test { + +namespace { + +using flutter::EncodableMap; +using flutter::EncodableValue; +using ::testing::DoAll; +using ::testing::Pointee; +using ::testing::Return; +using ::testing::SetArgPointee; + +class MockSystemApis : public SystemApis { + public: + MOCK_METHOD(LSTATUS, RegCloseKey, (HKEY key), (override)); + MOCK_METHOD(LSTATUS, RegQueryValueExW, + (HKEY key, LPCWSTR value_name, LPDWORD type, LPBYTE data, + LPDWORD data_size), + (override)); + MOCK_METHOD(LSTATUS, RegOpenKeyExW, + (HKEY key, LPCWSTR sub_key, DWORD options, REGSAM desired, + PHKEY result), + (override)); + MOCK_METHOD(HINSTANCE, ShellExecuteW, + (HWND hwnd, LPCWSTR operation, LPCWSTR file, LPCWSTR parameters, + LPCWSTR directory, int show_flags), + (override)); +}; + +class MockMethodResult : public flutter::MethodResult<> { + public: + MOCK_METHOD(void, SuccessInternal, (const EncodableValue* result), + (override)); + MOCK_METHOD(void, ErrorInternal, + (const std::string& error_code, const std::string& error_message, + const EncodableValue* details), + (override)); + MOCK_METHOD(void, NotImplementedInternal, (), (override)); +}; + +std::unique_ptr CreateArgumentsWithUrl(const std::string& url) { + EncodableMap args = { + {EncodableValue("url"), EncodableValue(url)}, + }; + return std::make_unique(args); +} + +} // namespace + +TEST(UrlLauncherPlugin, CanLaunchSuccessTrue) { + std::unique_ptr system = std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + // Return success values from the registery commands. + HKEY fake_key = reinterpret_cast(1); + EXPECT_CALL(*system, RegOpenKeyExW) + .WillOnce(DoAll(SetArgPointee<4>(fake_key), Return(ERROR_SUCCESS))); + EXPECT_CALL(*system, RegQueryValueExW).WillOnce(Return(ERROR_SUCCESS)); + EXPECT_CALL(*system, RegCloseKey(fake_key)).WillOnce(Return(ERROR_SUCCESS)); + // Expect a success response. + EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true)))); + + UrlLauncherPlugin plugin(std::move(system)); + plugin.HandleMethodCall( + flutter::MethodCall("canLaunch", + CreateArgumentsWithUrl("https://some.url.com")), + std::move(result)); +} + +TEST(UrlLauncherPlugin, CanLaunchQueryFailure) { + std::unique_ptr system = std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + // Return success values from the registery commands, except for the query, + // to simulate a scheme that is in the registry, but has no URL handler. + HKEY fake_key = reinterpret_cast(1); + EXPECT_CALL(*system, RegOpenKeyExW) + .WillOnce(DoAll(SetArgPointee<4>(fake_key), Return(ERROR_SUCCESS))); + EXPECT_CALL(*system, RegQueryValueExW).WillOnce(Return(ERROR_FILE_NOT_FOUND)); + EXPECT_CALL(*system, RegCloseKey(fake_key)).WillOnce(Return(ERROR_SUCCESS)); + // Expect a success response. + EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false)))); + + UrlLauncherPlugin plugin(std::move(system)); + plugin.HandleMethodCall( + flutter::MethodCall("canLaunch", + CreateArgumentsWithUrl("https://some.url.com")), + std::move(result)); +} + +TEST(UrlLauncherPlugin, CanLaunchHandlesOpenFailure) { + std::unique_ptr system = std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + // Return failure for opening. + EXPECT_CALL(*system, RegOpenKeyExW).WillOnce(Return(ERROR_BAD_PATHNAME)); + // Expect a success response. + EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false)))); + + UrlLauncherPlugin plugin(std::move(system)); + plugin.HandleMethodCall( + flutter::MethodCall("canLaunch", + CreateArgumentsWithUrl("https://some.url.com")), + std::move(result)); +} + +TEST(UrlLauncherPlugin, LaunchSuccess) { + std::unique_ptr system = std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + // Return a success value (>32) from launching. + EXPECT_CALL(*system, ShellExecuteW) + .WillOnce(Return(reinterpret_cast(33))); + // Expect a success response. + EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true)))); + + UrlLauncherPlugin plugin(std::move(system)); + plugin.HandleMethodCall( + flutter::MethodCall("launch", + CreateArgumentsWithUrl("https://some.url.com")), + std::move(result)); +} + +TEST(UrlLauncherPlugin, LaunchReportsFailure) { + std::unique_ptr system = std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + // Return a faile value (<=32) from launching. + EXPECT_CALL(*system, ShellExecuteW) + .WillOnce(Return(reinterpret_cast(32))); + // Expect an error response. + EXPECT_CALL(*result, ErrorInternal); + + UrlLauncherPlugin plugin(std::move(system)); + plugin.HandleMethodCall( + flutter::MethodCall("launch", + CreateArgumentsWithUrl("https://some.url.com")), + std::move(result)); +} + +} // namespace test +} // namespace url_launcher_plugin diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp index 51740a3a4b04..748c75ddd243 100644 --- a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp @@ -1,7 +1,7 @@ // 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. -#include "include/url_launcher_windows/url_launcher_plugin.h" +#include "url_launcher_plugin.h" #include #include @@ -9,9 +9,12 @@ #include #include +#include #include #include +namespace url_launcher_plugin { + namespace { using flutter::EncodableMap; @@ -54,19 +57,7 @@ std::string GetUrlArgument(const flutter::MethodCall<>& method_call) { return url; } -class UrlLauncherPlugin : public flutter::Plugin { - public: - static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar); - - virtual ~UrlLauncherPlugin(); - - private: - UrlLauncherPlugin(); - - // Called when a method is called on plugin channel; - void HandleMethodCall(const flutter::MethodCall<>& method_call, - std::unique_ptr> result); -}; +} // namespace // static void UrlLauncherPlugin::RegisterWithRegistrar( @@ -75,8 +66,8 @@ void UrlLauncherPlugin::RegisterWithRegistrar( registrar->messenger(), "plugins.flutter.io/url_launcher", &flutter::StandardMethodCodec::GetInstance()); - // Uses new instead of make_unique due to private constructor. - std::unique_ptr plugin(new UrlLauncherPlugin()); + std::unique_ptr plugin = + std::make_unique(); channel->SetMethodCallHandler( [plugin_pointer = plugin.get()](const auto& call, auto result) { @@ -86,7 +77,11 @@ void UrlLauncherPlugin::RegisterWithRegistrar( registrar->AddPlugin(std::move(plugin)); } -UrlLauncherPlugin::UrlLauncherPlugin() = default; +UrlLauncherPlugin::UrlLauncherPlugin() + : system_apis_(std::make_unique()) {} + +UrlLauncherPlugin::UrlLauncherPlugin(std::unique_ptr system_apis) + : system_apis_(std::move(system_apis)) {} UrlLauncherPlugin::~UrlLauncherPlugin() = default; @@ -99,17 +94,10 @@ void UrlLauncherPlugin::HandleMethodCall( result->Error("argument_error", "No URL provided"); return; } - std::wstring url_wide = Utf16FromUtf8(url); - - int status = static_cast(reinterpret_cast( - ::ShellExecute(nullptr, TEXT("open"), url_wide.c_str(), nullptr, - nullptr, SW_SHOWNORMAL))); - if (status <= 32) { - std::ostringstream error_message; - error_message << "Failed to open " << url << ": ShellExecute error code " - << status; - result->Error("open_error", error_message.str()); + std::optional error = LaunchUrl(url); + if (error) { + result->Error("open_error", error.value()); return; } result->Success(EncodableValue(true)); @@ -120,29 +108,48 @@ void UrlLauncherPlugin::HandleMethodCall( return; } - bool can_launch = false; - size_t separator_location = url.find(":"); - if (separator_location != std::string::npos) { - std::wstring scheme = Utf16FromUtf8(url.substr(0, separator_location)); - HKEY key = nullptr; - if (::RegOpenKeyEx(HKEY_CLASSES_ROOT, scheme.c_str(), 0, KEY_QUERY_VALUE, - &key) == ERROR_SUCCESS) { - can_launch = ::RegQueryValueEx(key, L"URL Protocol", nullptr, nullptr, - nullptr, nullptr) == ERROR_SUCCESS; - ::RegCloseKey(key); - } - } + bool can_launch = CanLaunchUrl(url); result->Success(EncodableValue(can_launch)); } else { result->NotImplemented(); } } -} // namespace +bool UrlLauncherPlugin::CanLaunchUrl(const std::string& url) { + size_t separator_location = url.find(":"); + if (separator_location == std::string::npos) { + return false; + } + std::wstring scheme = Utf16FromUtf8(url.substr(0, separator_location)); + + HKEY key = nullptr; + if (system_apis_->RegOpenKeyExW(HKEY_CLASSES_ROOT, scheme.c_str(), 0, + KEY_QUERY_VALUE, &key) != ERROR_SUCCESS) { + return false; + } + bool has_handler = + system_apis_->RegQueryValueExW(key, L"URL Protocol", nullptr, nullptr, + nullptr) == ERROR_SUCCESS; + system_apis_->RegCloseKey(key); + return has_handler; +} -void UrlLauncherPluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar) { - UrlLauncherPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarManager::GetInstance() - ->GetRegistrar(registrar)); +std::optional UrlLauncherPlugin::LaunchUrl( + const std::string& url) { + std::wstring url_wide = Utf16FromUtf8(url); + + int status = static_cast(reinterpret_cast( + system_apis_->ShellExecuteW(nullptr, TEXT("open"), url_wide.c_str(), + nullptr, nullptr, SW_SHOWNORMAL))); + + // Per ::ShellExecuteW documentation, anything >32 indicates success. + if (status <= 32) { + std::ostringstream error_message; + error_message << "Failed to open " << url << ": ShellExecute error code " + << status; + return std::optional(error_message.str()); + } + return std::nullopt; } + +} // namespace url_launcher_plugin diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h new file mode 100644 index 000000000000..45e70e5fc067 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h @@ -0,0 +1,48 @@ +// 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. +#include +#include +#include + +#include +#include +#include +#include + +#include "system_apis.h" + +namespace url_launcher_plugin { + +class UrlLauncherPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar); + + UrlLauncherPlugin(); + + // Creates a plugin instance with the given SystemApi instance. + // + // Exists for unit testing with mock implementations. + UrlLauncherPlugin(std::unique_ptr system_apis); + + virtual ~UrlLauncherPlugin(); + + // Disallow copy and move. + UrlLauncherPlugin(const UrlLauncherPlugin&) = delete; + UrlLauncherPlugin& operator=(const UrlLauncherPlugin&) = delete; + + // Called when a method is called on the plugin channel. + void HandleMethodCall(const flutter::MethodCall<>& method_call, + std::unique_ptr> result); + + private: + // Returns whether or not the given URL has a registered handler. + bool CanLaunchUrl(const std::string& url); + + // Attempts to launch the given URL. On failure, returns an error string. + std::optional LaunchUrl(const std::string& url); + + std::unique_ptr system_apis_; +}; + +} // namespace url_launcher_plugin diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp b/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp new file mode 100644 index 000000000000..05de586d8fe0 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp @@ -0,0 +1,15 @@ +// 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. +#include "include/url_launcher_windows/url_launcher_windows.h" + +#include + +#include "url_launcher_plugin.h" + +void UrlLauncherWindowsRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + url_launcher_plugin::UrlLauncherPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} From af2896b199ecc3cc5e1ed3000b009f65fc05c9cd Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 18 Aug 2021 06:51:10 -0700 Subject: [PATCH 079/123] [flutter_plugin_tools] Add a command to lint Android code (#4206) Adds a new `lint-android` command to run `gradlew lint` on Android plugins. Also standardizes the names of the Cirrus tasks that run all the build and platform-specific (i.e., not Dart unit test) tests for each platform, as they were getting unnecessarily long and complex in some cases. Fixes https://github.com/flutter/flutter/issues/87071 --- .cirrus.yml | 23 +- packages/android_alarm_manager/CHANGELOG.md | 1 + .../android/build.gradle | 2 + .../android/lint-baseline.xml | 59 +++ packages/android_intent/CHANGELOG.md | 1 + packages/android_intent/android/build.gradle | 1 + packages/battery/battery/CHANGELOG.md | 1 + packages/battery/battery/android/build.gradle | 1 + packages/camera/camera/CHANGELOG.md | 4 + packages/camera/camera/android/build.gradle | 2 + .../camera/camera/android/lint-baseline.xml | 114 +++++ .../connectivity/connectivity/CHANGELOG.md | 1 + .../connectivity/android/build.gradle | 1 + packages/device_info/device_info/CHANGELOG.md | 1 + .../device_info/android/build.gradle | 1 + packages/espresso/CHANGELOG.md | 4 + packages/espresso/android/build.gradle | 2 + packages/espresso/android/lint-baseline.xml | 389 ++++++++++++++++++ .../CHANGELOG.md | 4 + .../android/build.gradle | 1 + .../google_maps_flutter/android/build.gradle | 1 + .../google_sign_in/CHANGELOG.md | 4 + .../google_sign_in/android/build.gradle | 1 + .../image_picker/image_picker/CHANGELOG.md | 4 + .../image_picker/android/build.gradle | 1 + .../in_app_purchase_android/CHANGELOG.md | 8 +- .../android/build.gradle | 1 + packages/local_auth/CHANGELOG.md | 4 + packages/local_auth/android/build.gradle | 2 + packages/local_auth/android/lint-baseline.xml | 59 +++ packages/package_info/CHANGELOG.md | 1 + packages/package_info/android/build.gradle | 1 + .../path_provider/path_provider/CHANGELOG.md | 4 + .../path_provider/android/build.gradle | 1 + .../quick_actions/quick_actions/CHANGELOG.md | 4 + .../quick_actions/android/build.gradle | 1 + packages/sensors/CHANGELOG.md | 1 + packages/sensors/android/build.gradle | 1 + packages/share/CHANGELOG.md | 1 + packages/share/android/build.gradle | 1 + .../shared_preferences/CHANGELOG.md | 1 + .../shared_preferences/android/build.gradle | 2 + .../android/lint-baseline.xml | 81 ++++ .../url_launcher/url_launcher/CHANGELOG.md | 4 + .../url_launcher/android/build.gradle | 1 + .../video_player/video_player/CHANGELOG.md | 4 + .../video_player/android/build.gradle | 1 + .../webview_flutter/CHANGELOG.md | 4 + .../webview_flutter/android/build.gradle | 1 + .../wifi_info_flutter/CHANGELOG.md | 4 + .../wifi_info_flutter/android/build.gradle | 1 + script/tool/CHANGELOG.md | 1 + script/tool/lib/src/common/gradle.dart | 57 +++ script/tool/lib/src/common/xcode.dart | 2 +- .../lib/src/firebase_test_lab_command.dart | 46 +-- script/tool/lib/src/lint_android_command.dart | 61 +++ script/tool/lib/src/main.dart | 2 + script/tool/lib/src/native_test_command.dart | 29 +- script/tool/test/common/gradle_test.dart | 179 ++++++++ .../tool/test/lint_android_command_test.dart | 158 +++++++ 60 files changed, 1306 insertions(+), 47 deletions(-) create mode 100644 packages/android_alarm_manager/android/lint-baseline.xml create mode 100644 packages/camera/camera/android/lint-baseline.xml create mode 100644 packages/espresso/android/lint-baseline.xml create mode 100644 packages/local_auth/android/lint-baseline.xml create mode 100644 packages/shared_preferences/shared_preferences/android/lint-baseline.xml create mode 100644 script/tool/lib/src/common/gradle.dart create mode 100644 script/tool/lib/src/lint_android_command.dart create mode 100644 script/tool/test/common/gradle_test.dart create mode 100644 script/tool/test/lint_android_command_test.dart diff --git a/.cirrus.yml b/.cirrus.yml index ffdd71daebc4..d830a2a15913 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -119,7 +119,7 @@ task: setup_script: - flutter config --enable-linux-desktop << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - - name: build-linux+drive-examples + - name: linux-build+platform-tests env: matrix: CHANNEL: "master" @@ -146,7 +146,7 @@ task: memory: 12G matrix: ### Android tasks ### - - name: build-apks+android-unit+firebase-test-lab + - name: android-build+platform-tests env: matrix: PLUGIN_SHARDING: "--shardIndex 0 --shardCount 4" @@ -165,6 +165,13 @@ task: - export CIRRUS_CHANGE_MESSAGE="" - export CIRRUS_COMMIT_MESSAGE="" - ./script/tool_runner.sh build-examples --apk + lint_script: + # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they + # might include non-ASCII characters which makes Gradle crash. + # TODO(stuartmorgan): See https://github.com/flutter/flutter/issues/24935 + - export CIRRUS_CHANGE_MESSAGE="" + - export CIRRUS_COMMIT_MESSAGE="" + - ./script/tool_runner.sh lint-android # must come after build-examples native_unit_test_script: # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they # might include non-ASCII characters which makes Gradle crash. @@ -186,8 +193,14 @@ task: - else - echo "This user does not have permission to run Firebase Test Lab tests." - fi + # Upload the full lint results to Cirrus to display in the results UI. + always: + android-lint_artifacts: + path: "**/reports/lint-results-debug.xml" + type: text/xml + format: android-lint ### Web tasks ### - - name: build-web+drive-examples + - name: web-build+platform-tests env: matrix: CHANNEL: "master" @@ -220,7 +233,7 @@ task: CHANNEL: "master" CHANNEL: "stable" << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - - name: build-ipas+drive-examples + - name: ios-build+platform-tests env: PATH: $PATH:/usr/local/bin matrix: @@ -256,7 +269,7 @@ task: setup_script: - flutter config --enable-macos-desktop << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - - name: build-macos+drive-examples + - name: macos-build+platform-tests env: matrix: CHANNEL: "master" diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md index 71f47cede66e..d53b932e3f0f 100644 --- a/packages/android_alarm_manager/CHANGELOG.md +++ b/packages/android_alarm_manager/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove support for the V1 Android embedding. +* Updated Android lint settings. ## 2.0.2 diff --git a/packages/android_alarm_manager/android/build.gradle b/packages/android_alarm_manager/android/build.gradle index be741097f362..b173137786a9 100644 --- a/packages/android_alarm_manager/android/build.gradle +++ b/packages/android_alarm_manager/android/build.gradle @@ -38,6 +38,8 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") } diff --git a/packages/android_alarm_manager/android/lint-baseline.xml b/packages/android_alarm_manager/android/lint-baseline.xml new file mode 100644 index 000000000000..de588614fdb2 --- /dev/null +++ b/packages/android_alarm_manager/android/lint-baseline.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/android_intent/CHANGELOG.md b/packages/android_intent/CHANGELOG.md index 71428c53cea8..82cd5db3e4e4 100644 --- a/packages/android_intent/CHANGELOG.md +++ b/packages/android_intent/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove references to the V1 Android embedding. +* Updated Android lint settings. ## 2.0.2 diff --git a/packages/android_intent/android/build.gradle b/packages/android_intent/android/build.gradle index b0238b7db4f3..e8b9f3810146 100644 --- a/packages/android_intent/android/build.gradle +++ b/packages/android_intent/android/build.gradle @@ -35,6 +35,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/battery/battery/CHANGELOG.md b/packages/battery/battery/CHANGELOG.md index 8590e646564e..ddc912d2ba2a 100644 --- a/packages/battery/battery/CHANGELOG.md +++ b/packages/battery/battery/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove references to the Android v1 embedding. +* Updated Android lint settings. ## 2.0.3 diff --git a/packages/battery/battery/android/build.gradle b/packages/battery/battery/android/build.gradle index 1e484897c2ad..14f503813f7e 100644 --- a/packages/battery/battery/android/build.gradle +++ b/packages/battery/battery/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index d455ddb2fad1..694898092d7a 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 0.8.1+7 * Fix device orientation sometimes not affecting the camera preview orientation. diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index 6ceed97c9a17..9bbafb653ef8 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera/android/build.gradle @@ -35,6 +35,8 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") } compileOptions { sourceCompatibility = '1.8' diff --git a/packages/camera/camera/android/lint-baseline.xml b/packages/camera/camera/android/lint-baseline.xml new file mode 100644 index 000000000000..4ddaafa87988 --- /dev/null +++ b/packages/camera/camera/android/lint-baseline.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/connectivity/connectivity/CHANGELOG.md b/packages/connectivity/connectivity/CHANGELOG.md index 58047482fcb7..f5489692bee9 100644 --- a/packages/connectivity/connectivity/CHANGELOG.md +++ b/packages/connectivity/connectivity/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove references to the Android V1 embedding. +* Updated Android lint settings. ## 3.0.6 diff --git a/packages/connectivity/connectivity/android/build.gradle b/packages/connectivity/connectivity/android/build.gradle index 53a390bd74f0..983f29b142de 100644 --- a/packages/connectivity/connectivity/android/build.gradle +++ b/packages/connectivity/connectivity/android/build.gradle @@ -35,6 +35,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/device_info/device_info/CHANGELOG.md b/packages/device_info/device_info/CHANGELOG.md index 669423cc4efb..97349d450cf1 100644 --- a/packages/device_info/device_info/CHANGELOG.md +++ b/packages/device_info/device_info/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove references to the Android V1 embedding. +* Updated Android lint settings. ## 2.0.2 diff --git a/packages/device_info/device_info/android/build.gradle b/packages/device_info/device_info/android/build.gradle index 51ec2a7fb567..ed89da419d4a 100644 --- a/packages/device_info/device_info/android/build.gradle +++ b/packages/device_info/device_info/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/espresso/CHANGELOG.md b/packages/espresso/CHANGELOG.md index 10e5ae59f71a..e00ea7065ce0 100644 --- a/packages/espresso/CHANGELOG.md +++ b/packages/espresso/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 0.1.0+3 * Remove references to the Android v1 embedding. diff --git a/packages/espresso/android/build.gradle b/packages/espresso/android/build.gradle index 8cd54811afa0..da0cd2ebfee8 100644 --- a/packages/espresso/android/build.gradle +++ b/packages/espresso/android/build.gradle @@ -30,6 +30,8 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") } diff --git a/packages/espresso/android/lint-baseline.xml b/packages/espresso/android/lint-baseline.xml new file mode 100644 index 000000000000..19b349f044bf --- /dev/null +++ b/packages/espresso/android/lint-baseline.xml @@ -0,0 +1,389 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md index 6a05ed01e2de..7e567d8cce5c 100644 --- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md +++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 2.0.3 * Remove references to the Android V1 embedding. diff --git a/packages/flutter_plugin_android_lifecycle/android/build.gradle b/packages/flutter_plugin_android_lifecycle/android/build.gradle index ba3a54b235e6..5a584b4e366f 100644 --- a/packages/flutter_plugin_android_lifecycle/android/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/android/build.gradle @@ -31,6 +31,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } dependencies { diff --git a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle index 6c5ea76ae61e..e3cf6ffe8818 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } dependencies { diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index e4207de117fa..8ac07ae1793b 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 5.0.7 * Mark iOS arm64 simulators as unsupported. diff --git a/packages/google_sign_in/google_sign_in/android/build.gradle b/packages/google_sign_in/google_sign_in/android/build.gradle index 7d1825defa84..ea98b315f147 100644 --- a/packages/google_sign_in/google_sign_in/android/build.gradle +++ b/packages/google_sign_in/google_sign_in/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 9d89389cb105..4f21ed3cc398 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 0.8.3+2 * Fix using Camera as image source on Android 11+ diff --git a/packages/image_picker/image_picker/android/build.gradle b/packages/image_picker/image_picker/android/build.gradle index 607b3c1523a1..1e6439e6a4eb 100755 --- a/packages/image_picker/image_picker/android/build.gradle +++ b/packages/image_picker/image_picker/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } dependencies { implementation 'androidx.core:core:1.0.2' diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index d67d1efd61b5..60dae1be6d86 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,8 +1,12 @@ -# 0.1.4+4 +## NEXT + +* Updated Android lint settings. + +## 0.1.4+4 * Removed dependency on the `test` package. -# 0.1.4+3 +## 0.1.4+3 - Updated installation instructions in README. diff --git a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle index 349f9eeb734c..656f7c34bf7a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle +++ b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/packages/local_auth/CHANGELOG.md b/packages/local_auth/CHANGELOG.md index c33fa7778b94..c0d04fb5688a 100644 --- a/packages/local_auth/CHANGELOG.md +++ b/packages/local_auth/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 1.1.7 * Remove references to the Android V1 embedding. diff --git a/packages/local_auth/android/build.gradle b/packages/local_auth/android/build.gradle index 4fcb77cf6c98..dc282e78ced0 100644 --- a/packages/local_auth/android/build.gradle +++ b/packages/local_auth/android/build.gradle @@ -30,6 +30,8 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") } diff --git a/packages/local_auth/android/lint-baseline.xml b/packages/local_auth/android/lint-baseline.xml new file mode 100644 index 000000000000..e89eaadb3e6d --- /dev/null +++ b/packages/local_auth/android/lint-baseline.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/package_info/CHANGELOG.md b/packages/package_info/CHANGELOG.md index 2ec20b3fe775..0fe91175cf6b 100644 --- a/packages/package_info/CHANGELOG.md +++ b/packages/package_info/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove references to the Android v1 embedding. +* Updated Android lint settings. ## 2.0.2 diff --git a/packages/package_info/android/build.gradle b/packages/package_info/android/build.gradle index d2846f260556..e21d911ff490 100644 --- a/packages/package_info/android/build.gradle +++ b/packages/package_info/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index 5e08c520dcd7..ba7bb3dc7ada 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 2.0.3 * Add iOS unit test target. diff --git a/packages/path_provider/path_provider/android/build.gradle b/packages/path_provider/path_provider/android/build.gradle index db2c79c15796..3458140bd0eb 100644 --- a/packages/path_provider/path_provider/android/build.gradle +++ b/packages/path_provider/path_provider/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } android { compileOptions { diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md index 5d040f4fd74e..9087c2807061 100644 --- a/packages/quick_actions/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 0.6.0+5 * Support only calling initialize once. diff --git a/packages/quick_actions/quick_actions/android/build.gradle b/packages/quick_actions/quick_actions/android/build.gradle index 0bce642f3e60..ec3f84eab4cf 100644 --- a/packages/quick_actions/quick_actions/android/build.gradle +++ b/packages/quick_actions/quick_actions/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } dependencies { diff --git a/packages/sensors/CHANGELOG.md b/packages/sensors/CHANGELOG.md index 5ac0943333fa..acea470855fb 100644 --- a/packages/sensors/CHANGELOG.md +++ b/packages/sensors/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove references to the Android V1 embedding. +* Updated Android lint settings. ## 2.0.3 diff --git a/packages/sensors/android/build.gradle b/packages/sensors/android/build.gradle index a16ebd2ee459..7e1087764dee 100644 --- a/packages/sensors/android/build.gradle +++ b/packages/sensors/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/share/CHANGELOG.md b/packages/share/CHANGELOG.md index 9074f59f05b7..c9a468d925a7 100644 --- a/packages/share/CHANGELOG.md +++ b/packages/share/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove references to the Android V1 embedding. +* Updated Android lint settings. ## 2.0.4 diff --git a/packages/share/android/build.gradle b/packages/share/android/build.gradle index 1b95bf592fb6..b2ea363a3e11 100644 --- a/packages/share/android/build.gradle +++ b/packages/share/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } dependencies { diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 3476f4eff3f0..48abf9ad4045 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Add iOS unit test target. +* Updated Android lint settings. ## 2.0.6 diff --git a/packages/shared_preferences/shared_preferences/android/build.gradle b/packages/shared_preferences/shared_preferences/android/build.gradle index 6a66eba508fb..9284f1c36143 100644 --- a/packages/shared_preferences/shared_preferences/android/build.gradle +++ b/packages/shared_preferences/shared_preferences/android/build.gradle @@ -38,6 +38,8 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") } dependencies { testImplementation 'junit:junit:4.12' diff --git a/packages/shared_preferences/shared_preferences/android/lint-baseline.xml b/packages/shared_preferences/shared_preferences/android/lint-baseline.xml new file mode 100644 index 000000000000..6b2f35f5a151 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/android/lint-baseline.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index dc67a2142ec2..237f0b139475 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 6.0.10 * Remove references to the Android v1 embedding. diff --git a/packages/url_launcher/url_launcher/android/build.gradle b/packages/url_launcher/url_launcher/android/build.gradle index 5dd7e773a1ca..d374d40534c3 100644 --- a/packages/url_launcher/url_launcher/android/build.gradle +++ b/packages/url_launcher/url_launcher/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index f2029622f0ee..f07bb5f66f8c 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 2.1.14 * Removed dependency on the `flutter_test` package. diff --git a/packages/video_player/video_player/android/build.gradle b/packages/video_player/video_player/android/build.gradle index f2f18bff9798..9d9984439370 100644 --- a/packages/video_player/video_player/android/build.gradle +++ b/packages/video_player/video_player/android/build.gradle @@ -35,6 +35,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } android { compileOptions { diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index df7d9cb87457..361bfd24f3af 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 2.0.12 * Improved the documentation on using the different Android Platform View modes. diff --git a/packages/webview_flutter/webview_flutter/android/build.gradle b/packages/webview_flutter/webview_flutter/android/build.gradle index cd1b4188a1eb..4a164317c60f 100644 --- a/packages/webview_flutter/webview_flutter/android/build.gradle +++ b/packages/webview_flutter/webview_flutter/android/build.gradle @@ -31,6 +31,7 @@ android { lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } dependencies { diff --git a/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md b/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md index 925745faa22a..86f3f67af103 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md +++ b/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 2.0.2 * Update README to point to Plus Plugins version. diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle b/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle index 2b5a8a7fc209..661ee82da4d0 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle +++ b/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle @@ -29,6 +29,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 267019fe7359..87917d63d3fc 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT - Added Android native integration test support to `native-test`. +- Added a new `android-lint` command to lint Android plugin native code. ## 0.5.0 diff --git a/script/tool/lib/src/common/gradle.dart b/script/tool/lib/src/common/gradle.dart new file mode 100644 index 000000000000..e7214bf29714 --- /dev/null +++ b/script/tool/lib/src/common/gradle.dart @@ -0,0 +1,57 @@ +// 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:file/file.dart'; +import 'package:platform/platform.dart'; + +import 'process_runner.dart'; + +const String _gradleWrapperWindows = 'gradlew.bat'; +const String _gradleWrapperNonWindows = 'gradlew'; + +/// A utility class for interacting with Gradle projects. +class GradleProject { + /// Creates an instance that runs commands for [project] with the given + /// [processRunner]. + /// + /// If [log] is true, commands run by this instance will long various status + /// messages. + GradleProject( + this.flutterProject, { + this.processRunner = const ProcessRunner(), + this.platform = const LocalPlatform(), + }); + + /// The directory of a Flutter project to run Gradle commands in. + final Directory flutterProject; + + /// The [ProcessRunner] used to run commands. Overridable for testing. + final ProcessRunner processRunner; + + /// The platform that commands are being run on. + final Platform platform; + + /// The project's 'android' directory. + Directory get androidDirectory => flutterProject.childDirectory('android'); + + /// The path to the Gradle wrapper file for the project. + File get gradleWrapper => androidDirectory.childFile( + platform.isWindows ? _gradleWrapperWindows : _gradleWrapperNonWindows); + + /// Whether or not the project is ready to have Gradle commands run on it + /// (i.e., whether the `flutter` tool has generated the necessary files). + bool isConfigured() => gradleWrapper.existsSync(); + + /// Runs a `gradlew` command with the given parameters. + Future runCommand( + String target, { + List arguments = const [], + }) { + return processRunner.runAndStream( + gradleWrapper.path, + [target, ...arguments], + workingDir: androidDirectory, + ); + } +} diff --git a/script/tool/lib/src/common/xcode.dart b/script/tool/lib/src/common/xcode.dart index d6bbae419eda..83f681bcb492 100644 --- a/script/tool/lib/src/common/xcode.dart +++ b/script/tool/lib/src/common/xcode.dart @@ -15,7 +15,7 @@ const String _xcRunCommand = 'xcrun'; /// A utility class for interacting with the installed version of Xcode. class Xcode { - /// Creates an instance that runs commends with the given [processRunner]. + /// Creates an instance that runs commands with the given [processRunner]. /// /// If [log] is true, commands run by this instance will long various status /// messages. diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 8459f6c70153..fd2de97be4b3 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -10,6 +10,7 @@ import 'package:platform/platform.dart'; import 'package:uuid/uuid.dart'; import 'common/core.dart'; +import 'common/gradle.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; @@ -74,8 +75,6 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { 'Runs tests in test_instrumentation folder using the ' 'instrumentation_test package.'; - static const String _gradleWrapper = 'gradlew'; - bool _firebaseProjectConfigured = false; Future _configureFirebaseProject() async { @@ -138,13 +137,15 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { } // Ensures that gradle wrapper exists - if (!await _ensureGradleWrapperExists(androidDirectory)) { + final GradleProject project = GradleProject(exampleDirectory, + processRunner: processRunner, platform: platform); + if (!await _ensureGradleWrapperExists(project)) { return PackageResult.fail(['Unable to build example apk']); } await _configureFirebaseProject(); - if (!await _runGradle(androidDirectory, 'app:assembleAndroidTest')) { + if (!await _runGradle(project, 'app:assembleAndroidTest')) { return PackageResult.fail(['Unable to assemble androidTest']); } @@ -156,8 +157,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { for (final File test in _findIntegrationTestFiles(package)) { final String testName = getRelativePosixPath(test, from: package); print('Testing $testName...'); - if (!await _runGradle(androidDirectory, 'app:assembleDebug', - testFile: test)) { + if (!await _runGradle(project, 'app:assembleDebug', testFile: test)) { printError('Could not build $testName'); errors.add('$testName failed to build'); continue; @@ -204,12 +204,12 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { : PackageResult.fail(errors); } - /// Checks that 'gradlew' exists in [androidDirectory], and if not runs a + /// Checks that Gradle has been configured for [project], and if not runs a /// Flutter build to generate it. /// /// Returns true if either gradlew was already present, or the build succeeds. - Future _ensureGradleWrapperExists(Directory androidDirectory) async { - if (!androidDirectory.childFile(_gradleWrapper).existsSync()) { + Future _ensureGradleWrapperExists(GradleProject project) async { + if (!project.isConfigured()) { print('Running flutter build apk...'); final String experiment = getStringArg(kEnableExperiment); final int exitCode = await processRunner.runAndStream( @@ -219,7 +219,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { 'apk', if (experiment.isNotEmpty) '--enable-experiment=$experiment', ], - workingDir: androidDirectory); + workingDir: project.androidDirectory); if (exitCode != 0) { return false; @@ -228,15 +228,15 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { return true; } - /// Builds [target] using 'gradlew' in the given [directory]. Assumes - /// 'gradlew' already exists. + /// Builds [target] using Gradle in the given [project]. Assumes Gradle is + /// already configured. /// /// [testFile] optionally does the Flutter build with the given test file as /// the build target. /// /// Returns true if the command succeeds. Future _runGradle( - Directory directory, + GradleProject project, String target, { File? testFile, }) async { @@ -245,17 +245,15 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { ? Uri.encodeComponent('--enable-experiment=$experiment') : null; - final int exitCode = await processRunner.runAndStream( - directory.childFile(_gradleWrapper).path, - [ - target, - '-Pverbose=true', - if (testFile != null) '-Ptarget=${testFile.path}', - if (extraOptions != null) '-Pextra-front-end-options=$extraOptions', - if (extraOptions != null) - '-Pextra-gen-snapshot-options=$extraOptions', - ], - workingDir: directory); + final int exitCode = await project.runCommand( + target, + arguments: [ + '-Pverbose=true', + if (testFile != null) '-Ptarget=${testFile.path}', + if (extraOptions != null) '-Pextra-front-end-options=$extraOptions', + if (extraOptions != null) '-Pextra-gen-snapshot-options=$extraOptions', + ], + ); if (exitCode != 0) { return false; diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart new file mode 100644 index 000000000000..be6c6ed32415 --- /dev/null +++ b/script/tool/lib/src/lint_android_command.dart @@ -0,0 +1,61 @@ +// 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:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:platform/platform.dart'; + +import 'common/core.dart'; +import 'common/gradle.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; + +/// Lint the CocoaPod podspecs and run unit tests. +/// +/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. +class LintAndroidCommand extends PackageLoopingCommand { + /// Creates an instance of the linter command. + LintAndroidCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform); + + @override + final String name = 'lint-android'; + + @override + final String description = 'Runs "gradlew lint" on Android plugins.\n\n' + 'Requires the example to have been build at least once before running.'; + + @override + Future runForPackage(Directory package) async { + if (!pluginSupportsPlatform(kPlatformAndroid, package, + requiredMode: PlatformSupport.inline)) { + return PackageResult.skip( + 'Plugin does not have an Android implemenatation.'); + } + + final Directory exampleDirectory = package.childDirectory('example'); + final GradleProject project = GradleProject(exampleDirectory, + processRunner: processRunner, platform: platform); + + if (!project.isConfigured()) { + return PackageResult.fail(['Build example before linting']); + } + + final String packageName = package.basename; + + // Only lint one build mode to avoid extra work. + // Only lint the plugin project itself, to avoid failing due to errors in + // dependencies. + // + // TODO(stuartmorgan): Consider adding an XML parser to read and summarize + // all results. Currently, only the first three errors will be shown inline, + // and the rest have to be checked via the CI-uploaded artifact. + final int exitCode = await project.runCommand('$packageName:lintDebug'); + + return exitCode == 0 ? PackageResult.success() : PackageResult.fail(); + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 6001c5df7f0a..e70cba24cc5e 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -16,6 +16,7 @@ import 'drive_examples_command.dart'; import 'firebase_test_lab_command.dart'; import 'format_command.dart'; import 'license_check_command.dart'; +import 'lint_android_command.dart'; import 'lint_podspecs_command.dart'; import 'list_command.dart'; import 'native_test_command.dart'; @@ -51,6 +52,7 @@ void main(List args) { ..addCommand(FirebaseTestLabCommand(packagesDir)) ..addCommand(FormatCommand(packagesDir)) ..addCommand(LicenseCheckCommand(packagesDir)) + ..addCommand(LintAndroidCommand(packagesDir)) ..addCommand(LintPodspecsCommand(packagesDir)) ..addCommand(ListCommand(packagesDir)) ..addCommand(NativeTestCommand(packagesDir)) diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 9fc6a2912ccc..0bd2ab45f634 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -6,6 +6,7 @@ import 'package:file/file.dart'; import 'package:platform/platform.dart'; import 'common/core.dart'; +import 'common/gradle.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; @@ -47,8 +48,6 @@ class NativeTestCommand extends PackageLoopingCommand { help: 'Runs native integration (UI) tests', defaultsTo: true); } - static const String _gradleWrapper = 'gradlew'; - // The device destination flags for iOS tests. List _iosDestinationFlags = []; @@ -243,9 +242,12 @@ this command. final String exampleName = getPackageDescription(example); _printRunningExampleTestsMessage(example, 'Android'); - final Directory androidDirectory = example.childDirectory('android'); - final File gradleFile = androidDirectory.childFile(_gradleWrapper); - if (!gradleFile.existsSync()) { + final GradleProject project = GradleProject( + example, + processRunner: processRunner, + platform: platform, + ); + if (!project.isConfigured()) { printError('ERROR: Run "flutter build apk" on $exampleName, or run ' 'this tool\'s "build-examples --apk" command, ' 'before executing tests.'); @@ -256,9 +258,7 @@ this command. if (runUnitTests) { print('Running unit tests...'); - final int exitCode = await processRunner.runAndStream( - gradleFile.path, ['testDebugUnitTest'], - workingDir: androidDirectory); + final int exitCode = await project.runCommand('testDebugUnitTest'); if (exitCode != 0) { printError('$exampleName unit tests failed.'); failed = true; @@ -275,13 +275,12 @@ this command. 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; print('Running integration tests...'); - final int exitCode = await processRunner.runAndStream( - gradleFile.path, - [ - 'app:connectedAndroidTest', - '-Pandroid.testInstrumentationRunnerArguments.$filter', - ], - workingDir: androidDirectory); + final int exitCode = await project.runCommand( + 'app:connectedAndroidTest', + arguments: [ + '-Pandroid.testInstrumentationRunnerArguments.$filter', + ], + ); if (exitCode != 0) { printError('$exampleName integration tests failed.'); failed = true; diff --git a/script/tool/test/common/gradle_test.dart b/script/tool/test/common/gradle_test.dart new file mode 100644 index 000000000000..c24887d3d469 --- /dev/null +++ b/script/tool/test/common/gradle_test.dart @@ -0,0 +1,179 @@ +// 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 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/gradle.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; +import '../util.dart'; + +void main() { + late FileSystem fileSystem; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + processRunner = RecordingProcessRunner(); + }); + + group('isConfigured', () { + test('reports true when configured on Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew.bat']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + expect(project.isConfigured(), true); + }); + + test('reports true when configured on non-Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + expect(project.isConfigured(), true); + }); + + test('reports false when not configured on Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/foo']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + expect(project.isConfigured(), false); + }); + + test('reports true when configured on non-Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/foo']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + expect(project.isConfigured(), false); + }); + }); + + group('runXcodeBuild', () { + test('runs without arguments', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + final int exitCode = await project.runCommand('foo'); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + plugin.childDirectory('android').childFile('gradlew').path, + const [ + 'foo', + ], + plugin.childDirectory('android').path), + ])); + }); + + test('runs with arguments', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + final int exitCode = await project.runCommand( + 'foo', + arguments: ['--bar', '--baz'], + ); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + plugin.childDirectory('android').childFile('gradlew').path, + const [ + 'foo', + '--bar', + '--baz', + ], + plugin.childDirectory('android').path), + ])); + }); + + test('runs with the correct wrapper on Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew.bat']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + final int exitCode = await project.runCommand('foo'); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + plugin.childDirectory('android').childFile('gradlew.bat').path, + const [ + 'foo', + ], + plugin.childDirectory('android').path), + ])); + }); + + test('returns error codes', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew.bat']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + processRunner.mockProcessesForExecutable[project.gradleWrapper.path] = + [ + MockProcess.failing(), + ]; + + final int exitCode = await project.runCommand('foo'); + + expect(exitCode, 1); + }); + }); +} diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart new file mode 100644 index 000000000000..05ead220c15b --- /dev/null +++ b/script/tool/test/lint_android_command_test.dart @@ -0,0 +1,158 @@ +// 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 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/lint_android_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$LintAndroidCommand', () { + FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late MockPlatform mockPlatform; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(style: FileSystemStyle.posix); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + mockPlatform = MockPlatform(); + processRunner = RecordingProcessRunner(); + final LintAndroidCommand command = LintAndroidCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'lint_android_test', 'Test for $LintAndroidCommand'); + runner.addCommand(command); + }); + + test('runs gradle lint', () async { + final Directory pluginDir = + createFakePlugin('plugin1', packagesDir, extraFiles: [ + 'example/android/gradlew', + ], platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }); + + final Directory androidDir = + pluginDir.childDirectory('example').childDirectory('android'); + + final List output = + await runCapturingPrint(runner, ['lint-android']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidDir.childFile('gradlew').path, + const ['plugin1:lintDebug'], + androidDir.path, + ), + ]), + ); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('No issues found!'), + ])); + }); + + test('fails if gradlew is missing', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['lint-android'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [ + contains('Build example before linting'), + ], + )); + }); + + test('fails if linting finds issues', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }); + + processRunner.mockProcessesForExecutable['gradlew'] = [ + MockProcess.failing(), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['lint-android'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [ + contains('Build example before linting'), + ], + )); + }); + + test('skips non-Android plugins', () async { + createFakePlugin('plugin1', packagesDir); + + final List output = + await runCapturingPrint(runner, ['lint-android']); + + expect( + output, + containsAllInOrder( + [ + contains( + 'SKIPPING: Plugin does not have an Android implemenatation.') + ], + )); + }); + + test('skips non-inline plugins', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.federated + }); + + final List output = + await runCapturingPrint(runner, ['lint-android']); + + expect( + output, + containsAllInOrder( + [ + contains( + 'SKIPPING: Plugin does not have an Android implemenatation.') + ], + )); + }); + }); +} From a22f5912f6ba5f6b15378172c507388991d50de3 Mon Sep 17 00:00:00 2001 From: Andrew Zuo Date: Thu, 19 Aug 2021 10:16:27 -0400 Subject: [PATCH 080/123] [in_app_purchase] Add toString() to IAPError (#4162) This adds toString() to the IAPError class. This is so it is easier to see what is causing IAPError's in error logs. --- .../CHANGELOG.md | 4 +++ .../lib/src/errors/errors.dart | 1 + .../in_app_purchase_error.dart | 5 ++++ .../src/types/product_details_response.dart | 2 +- .../lib/src/types/purchase_details.dart | 2 +- .../lib/src/types/types.dart | 1 - .../pubspec.yaml | 2 +- .../errors/in_app_purchase_error_test.dart | 29 +++++++++++++++++++ 8 files changed, 42 insertions(+), 4 deletions(-) rename packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/{types => errors}/in_app_purchase_error.dart (88%) create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/test/src/errors/in_app_purchase_error_test.dart diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md index ec619d2fdc37..cd4b86d7f39a 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.0 + +* Added `toString()` to `IAPError` + ## 1.1.0 * Added `currencySymbol` in ProductDetails. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart index 7b788aaef490..8e10997aaedc 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart @@ -2,4 +2,5 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'in_app_purchase_error.dart'; export 'in_app_purchase_exception.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_error.dart similarity index 88% rename from packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart rename to packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_error.dart index f305f578f54a..166646d35b24 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_error.dart @@ -28,4 +28,9 @@ class IAPError { /// Error details, possibly null. final dynamic details; + + @override + String toString() { + return 'IAPError(code: $code, source: $source, message: $message, details: $details)'; + } } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart index 11b244a84ae3..3a9d7c3c976e 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'in_app_purchase_error.dart'; +import '../errors/in_app_purchase_error.dart'; import 'product_details.dart'; /// The response returned by [InAppPurchasePlatform.queryProductDetails]. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart index 08d0efe09878..8c98beb591ef 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'in_app_purchase_error.dart'; +import '../errors/in_app_purchase_error.dart'; import 'purchase_status.dart'; import 'purchase_verification_data.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart index 33d183c51d04..7cb666408249 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export 'in_app_purchase_error.dart'; export 'product_details.dart'; export 'product_details_response.dart'; export 'purchase_details.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml index d15e5f40fc6f..64574e0cf306 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purch issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.1.0 +version: 1.2.0 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/errors/in_app_purchase_error_test.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/errors/in_app_purchase_error_test.dart new file mode 100644 index 000000000000..ed63f495b4c2 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/errors/in_app_purchase_error_test.dart @@ -0,0 +1,29 @@ +// 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:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_platform_interface/src/errors/in_app_purchase_error.dart'; + +void main() { + test('toString: Should return a description of the error', () { + final IAPError exceptionNoDetails = IAPError( + code: 'error_code', + message: 'dummy_message', + source: 'dummy_source', + ); + + expect(exceptionNoDetails.toString(), + 'IAPError(code: error_code, source: dummy_source, message: dummy_message, details: null)'); + + final IAPError exceptionWithDetails = IAPError( + code: 'error_code', + message: 'dummy_message', + source: 'dummy_source', + details: 'dummy_details', + ); + + expect(exceptionWithDetails.toString(), + 'IAPError(code: error_code, source: dummy_source, message: dummy_message, details: dummy_details)'); + }); +} From f93314bb3779ebb0151bc326a0e515ca5f46533c Mon Sep 17 00:00:00 2001 From: Yusuf <82844127+ydag@users.noreply.github.com> Date: Fri, 20 Aug 2021 16:17:06 +0200 Subject: [PATCH 081/123] [image_picker] Platform interface update cache (#4123) --- .../CHANGELOG.md | 4 ++++ .../method_channel_image_picker.dart | 14 +++++++++++++- .../image_picker_platform.dart | 6 +++--- .../lib/src/types/lost_data_response.dart | 17 +++++++++++++++-- .../pubspec.yaml | 2 +- .../new_method_channel_image_picker_test.dart | 16 ++++++++++++++++ 6 files changed, 52 insertions(+), 7 deletions(-) diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index bd56f0ca77a6..97480e044284 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.0 + +* Updated `LostDataResponse` to include a `files` property, in case more than one file was recovered. + ## 2.2.0 * Added new methods that return `XFile` (from `package:cross_file`) diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart index bb9e18e78d83..292cb814ddeb 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart @@ -227,7 +227,9 @@ class MethodChannelImagePicker extends ImagePickerPlatform { @override Future getLostData() async { - final Map? result = + List? pickedFileList; + + Map? result = await _channel.invokeMapMethod('retrieve'); if (result == null) { @@ -254,10 +256,20 @@ class MethodChannelImagePicker extends ImagePickerPlatform { final String? path = result['path']; + final pathList = result['pathList']; + if (pathList != null) { + pickedFileList = []; + // In this case, multiRetrieve is invoked. + for (String path in pathList) { + pickedFileList.add(XFile(path)); + } + } + return LostDataResponse( file: path != null ? XFile(path) : null, exception: exception, type: retrieveType, + files: pickedFileList, ); } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart index 8f9ab99eae06..5c1c8b698442 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart @@ -128,7 +128,8 @@ abstract class ImagePickerPlatform extends PlatformInterface { throw UnimplementedError('pickVideo() has not been implemented.'); } - /// Retrieve the lost [PickedFile] file when [pickImage] or [pickVideo] failed because the MainActivity is destroyed. (Android only) + /// Retrieves any previously picked file, that was lost due to the MainActivity being destroyed. + /// In case multiple files were lost, only the last file will be recovered. (Android only). /// /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. /// Call this method to retrieve the lost data and process the data according to your APP's business logic. @@ -233,8 +234,7 @@ abstract class ImagePickerPlatform extends PlatformInterface { throw UnimplementedError('getVideo() has not been implemented.'); } - /// Retrieve the lost [XFile] file when [getImage], [getMultiImage] or [getVideo] failed because the MainActivity is - /// destroyed. (Android only) + /// Retrieves any previously picked files, that were lost due to the MainActivity being destroyed. (Android only) /// /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is /// always alive. Call this method to retrieve the lost data and process the data according to your APP's business logic. diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart index 576ad334bd35..65f5d7e15c90 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart @@ -14,7 +14,12 @@ import 'package:image_picker_platform_interface/src/types/types.dart'; class LostDataResponse { /// Creates an instance with the given [file], [exception], and [type]. Any of /// the params may be null, but this is never considered to be empty. - LostDataResponse({this.file, this.exception, this.type}); + LostDataResponse({ + this.file, + this.exception, + this.type, + this.files, + }); /// Initializes an instance with all member params set to null and considered /// to be empty. @@ -22,7 +27,8 @@ class LostDataResponse { : file = null, exception = null, type = null, - _empty = true; + _empty = true, + files = null; /// Whether it is an empty response. /// @@ -50,4 +56,11 @@ class LostDataResponse { final RetrieveType? type; bool _empty = false; + + /// The list of files that were lost in a previous [getMultiImage] call due to MainActivity being destroyed. + /// + /// When [files] is populated, [file] will refer to the last item in the [files] list. + /// + /// Can be null if [exception] exists. + final List? files; } diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 0953e76f03ee..2168ff0f778a 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.2.0 +version: 2.3.0 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart index e5321abc0121..17caa8456621 100644 --- a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart @@ -929,6 +929,22 @@ void main() { expect(response.file!.path, '/example/path'); }); + test('getLostData should successfully retrieve multiple files', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path1', + 'pathList': ['/example/path0', '/example/path1'], + }; + }); + final LostDataResponse response = await picker.getLostData(); + expect(response.type, RetrieveType.image); + expect(response.file, isNotNull); + expect(response.file!.path, '/example/path1'); + expect(response.files!.first.path, '/example/path0'); + expect(response.files!.length, 2); + }); + test('getLostData get error response', () async { picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { return { From 5ea96e51de83ae5cfcfecf5ae4af15fdd5f8c9b8 Mon Sep 17 00:00:00 2001 From: Cobinja Date: Fri, 20 Aug 2021 16:57:08 +0200 Subject: [PATCH 082/123] [shared_preferences] Fix possible clash of string with double entry (#3895) --- .../shared_preferences/CHANGELOG.md | 3 +- .../MethodCallHandlerImpl.java | 4 ++- .../shared_preferences_test.dart | 36 +++++++++++++++++++ .../lib/shared_preferences.dart | 7 ++++ .../shared_preferences/pubspec.yaml | 2 +- 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 48abf9ad4045..57b35a81255b 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,7 +1,8 @@ -## NEXT +## 2.0.7 * Add iOS unit test target. * Updated Android lint settings. +* Fix string clash with double entries on Android ## 2.0.6 diff --git a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java index 71ec14e7d06b..cea3f34b9b96 100644 --- a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java +++ b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java @@ -86,7 +86,9 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { break; case "setString": String value = (String) call.argument("value"); - if (value.startsWith(LIST_IDENTIFIER) || value.startsWith(BIG_INTEGER_PREFIX)) { + if (value.startsWith(LIST_IDENTIFIER) + || value.startsWith(BIG_INTEGER_PREFIX) + || value.startsWith(DOUBLE_PREFIX)) { result.error( "StorageError", "This string cannot be stored as it clashes with special identifier prefixes.", diff --git a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart index 1d46ed5751b0..e8498f473a2c 100644 --- a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart @@ -3,6 +3,8 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:integration_test/integration_test.dart'; @@ -102,5 +104,39 @@ void main() { // The last write should win. expect(preferences.getInt('int'), writeCount); }); + + testWidgets( + 'string clash with lists, big integers and doubles (Android only)', + (WidgetTester _) async { + await preferences.clear(); + // special prefixes plus a string value + expect( + // prefix for lists + preferences.setString( + 'String', + 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu' + + kTestValues2['flutter.String']), + throwsA(isA())); + await preferences.reload(); + expect(preferences.getString('String'), null); + expect( + // prefix for big integers + preferences.setString( + 'String', + 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy' + + kTestValues2['flutter.String']), + throwsA(isA())); + await preferences.reload(); + expect(preferences.getString('String'), null); + expect( + // prefix for doubles + preferences.setString( + 'String', + 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu' + + kTestValues2['flutter.String']), + throwsA(isA())); + await preferences.reload(); + expect(preferences.getString('String'), null); + }, skip: !Platform.isAndroid); }); } diff --git a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart index 3d2dd051f61c..841d615262de 100644 --- a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart @@ -128,6 +128,13 @@ class SharedPreferences { _setValue('Double', key, value); /// Saves a string [value] to persistent storage in the background. + /// + /// Note: Due to limitations in Android's SharedPreferences, + /// values cannot start with any one of the following: + /// + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu' + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy' + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu' Future setString(String key, String value) => _setValue('String', key, value); diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index c3039a98588b..e3cdfe4f87b3 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.6 +version: 2.0.7 environment: sdk: ">=2.12.0 <3.0.0" From f2b42f78b17ea5edc8f1078aee97a4438f06a6ad Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 20 Aug 2021 07:58:24 -0700 Subject: [PATCH 083/123] Fix and test for 'implements' pubspec entry (#4242) The federated plugin spec calls for implementation packages to include an `implements` entry in the `plugins` section of the `pubspec.yaml` indicating what app-facing package it implements. Most of the described behaviors of the `flutter` tool aren't implemented yet, and the pub.dev features have `default_plugin` as a backstop, so we haven't noticed that they are mostly missing (or in one case, incorrect). To better future-proof the plugins, and to provide a better example to people looking at our plugins as examples of federation, this adds a CI check to make sure that we are correctly adding it, and fixes all of the missing/incorrect values it turned up. Fixes https://github.com/flutter/flutter/issues/88222 --- packages/camera/camera_web/CHANGELOG.md | 4 + packages/camera/camera_web/pubspec.yaml | 4 +- .../connectivity_for_web/CHANGELOG.md | 4 + .../connectivity_for_web/pubspec.yaml | 3 +- .../connectivity_macos/CHANGELOG.md | 3 +- .../connectivity_macos/pubspec.yaml | 4 +- .../file_selector_web/CHANGELOG.md | 4 + .../file_selector_web/pubspec.yaml | 3 +- .../google_maps_flutter_web/CHANGELOG.md | 4 + .../google_maps_flutter_web/pubspec.yaml | 3 +- .../google_sign_in_web/CHANGELOG.md | 4 + .../google_sign_in_web/pubspec.yaml | 3 +- .../image_picker_for_web/CHANGELOG.md | 4 + .../image_picker_for_web/pubspec.yaml | 3 +- .../in_app_purchase_android/CHANGELOG.md | 3 +- .../in_app_purchase_android/pubspec.yaml | 3 +- .../in_app_purchase_ios/CHANGELOG.md | 4 + .../in_app_purchase_ios/pubspec.yaml | 3 +- .../shared_preferences_web/CHANGELOG.md | 4 + .../shared_preferences_web/pubspec.yaml | 3 +- .../url_launcher_web/CHANGELOG.md | 4 + .../url_launcher_web/pubspec.yaml | 3 +- .../video_player_web/CHANGELOG.md | 4 + .../video_player_web/pubspec.yaml | 3 +- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/pubspec_check_command.dart | 80 ++++- .../tool/test/pubspec_check_command_test.dart | 273 +++++++++++++++--- 27 files changed, 369 insertions(+), 69 deletions(-) diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index 68bc5f4e1a1e..a481554b540c 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.0+1 + +* Add `implements` to pubspec. + ## 0.1.0 * Initial release diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index c4d78999f273..822af60a979b 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_web description: A Flutter plugin for getting information about and controlling the camera on Web. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.1.0 +version: 0.1.0+1 # This plugin is under development and will be published # when the first working web camera implementation is added. @@ -30,4 +30,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.11.1 \ No newline at end of file + pedantic: ^1.11.1 diff --git a/packages/connectivity/connectivity_for_web/CHANGELOG.md b/packages/connectivity/connectivity_for_web/CHANGELOG.md index ccd689760b84..97e5032c8dd4 100644 --- a/packages/connectivity/connectivity_for_web/CHANGELOG.md +++ b/packages/connectivity/connectivity_for_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.0+1 + +* Add `implements` to pubspec. + ## 0.4.0 * Migrate to null-safety diff --git a/packages/connectivity/connectivity_for_web/pubspec.yaml b/packages/connectivity/connectivity_for_web/pubspec.yaml index 5b05dd80d088..2aaa8bd978fa 100644 --- a/packages/connectivity/connectivity_for_web/pubspec.yaml +++ b/packages/connectivity/connectivity_for_web/pubspec.yaml @@ -2,7 +2,7 @@ name: connectivity_for_web description: An implementation for the web platform of the Flutter `connectivity` plugin. This uses the NetworkInformation Web API, with a fallback to Navigator.onLine. repository: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_for_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+connectivity%22 -version: 0.4.0 +version: 0.4.0+1 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: connectivity platforms: web: pluginClass: ConnectivityPlugin diff --git a/packages/connectivity/connectivity_macos/CHANGELOG.md b/packages/connectivity/connectivity_macos/CHANGELOG.md index c7bc5b4cf469..46a4038f91ee 100644 --- a/packages/connectivity/connectivity_macos/CHANGELOG.md +++ b/packages/connectivity/connectivity_macos/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 0.2.1+2 * Add Swift language version to podspec. +* Fix `implements` package name in pubspec. ## 0.2.1+1 diff --git a/packages/connectivity/connectivity_macos/pubspec.yaml b/packages/connectivity/connectivity_macos/pubspec.yaml index 1e8842c7417a..b98f23d34eb7 100644 --- a/packages/connectivity/connectivity_macos/pubspec.yaml +++ b/packages/connectivity/connectivity_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: connectivity_macos description: macOS implementation of the connectivity plugin. repository: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+connectivity%22 -version: 0.2.1+1 +version: 0.2.1+2 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,7 +10,7 @@ environment: flutter: plugin: - implements: connectivity_platform_interface + implements: connectivity platforms: macos: pluginClass: ConnectivityPlugin diff --git a/packages/file_selector/file_selector_web/CHANGELOG.md b/packages/file_selector/file_selector_web/CHANGELOG.md index dadf5ffdc3fc..e2a863643027 100644 --- a/packages/file_selector/file_selector_web/CHANGELOG.md +++ b/packages/file_selector/file_selector_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.1+2 + +* Add `implements` to pubspec. + # 0.8.1+1 - Updated installation instructions in README. diff --git a/packages/file_selector/file_selector_web/pubspec.yaml b/packages/file_selector/file_selector_web/pubspec.yaml index 9753f9216694..bbad45bf2d6b 100644 --- a/packages/file_selector/file_selector_web/pubspec.yaml +++ b/packages/file_selector/file_selector_web/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_web description: Web platform implementation of file_selector repository: https://github.com/flutter/plugins/tree/master/packages/file_selector/file_selector_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.8.1+1 +version: 0.8.1+2 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: file_selector platforms: web: pluginClass: FileSelectorWeb diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md index d587c16f9207..83ffe09b357d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.0+4 + +* Add `implements` to pubspec. + ## 0.3.0+3 * Update the `README.md` usage instructions to not be tied to explicit package versions. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index c4323fc6486f..82605f8fd070 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 0.3.0+3 +version: 0.3.0+4 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: google_maps_flutter platforms: web: pluginClass: GoogleMapsPlugin diff --git a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md index 8a2f1dbf56d2..7b9eb6b747ec 100644 --- a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.0+2 + +* Add `implements` to pubspec. + ## 0.10.0+1 * Updated installation instructions in README. diff --git a/packages/google_sign_in/google_sign_in_web/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/pubspec.yaml index 0de229e795ce..7075f43151a6 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android, iOS and Web. repository: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 0.10.0+1 +version: 0.10.0+2 environment: sdk: ">=2.12.0 <3.0.0" @@ -11,6 +11,7 @@ environment: flutter: plugin: + implements: google_sign_in platforms: web: pluginClass: GoogleSignInPlugin diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index 01d13f900d2d..d11ead3bb64e 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.3 + +* Add `implements` to pubspec. + ## 2.1.2 * Updated installation instructions in README. diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index 6296992c46d0..895486f3de06 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_for_web description: Web platform implementation of image_picker repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 2.1.2 +version: 2.1.3 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: image_picker platforms: web: pluginClass: ImagePickerPlugin diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index 60dae1be6d86..8e342a65422c 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.1.4+5 +* Add `implements` to pubspec. * Updated Android lint settings. ## 0.1.4+4 diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml index 3969e34c052b..745b651e5828 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.4+4 +version: 0.1.4+5 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: in_app_purchase platforms: android: package: io.flutter.plugins.inapppurchase diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md index c76409521e2f..e66b5dee6295 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.3+3 + +* Add `implements` to pubspec. + # 0.1.3+2 * Removed dependency on the `test` package. diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index 8fc42371f405..07eae3ccc702 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_ios description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.3+2 +version: 0.1.3+3 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: in_app_purchase platforms: ios: pluginClass: InAppPurchasePlugin diff --git a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md index 0a00e7d66a2a..dd68f5321541 100644 --- a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.2 + +* Add `implements` to pubspec. + ## 2.0.1 * Updated installation instructions in README. diff --git a/packages/shared_preferences/shared_preferences_web/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/pubspec.yaml index 2e67be20e427..c878903ac236 100644 --- a/packages/shared_preferences/shared_preferences_web/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_web description: Web platform implementation of shared_preferences repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.1 +version: 2.0.2 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: shared_preferences platforms: web: pluginClass: SharedPreferencesPlugin diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index 64830f5e4481..f5338e62a775 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.4 + +* Add `implements` to pubspec. + ## 2.0.3 - Replaced reference to `shared_preferences` plugin with the `url_launcher` in the README. diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml index cba098daceb7..77e8068f1396 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_web description: Web platform implementation of url_launcher repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 2.0.3 +version: 2.0.4 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: url_launcher platforms: web: pluginClass: UrlLauncherPlugin diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 398ec02ba743..a7a198db21e1 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.3 + +* Add `implements` to pubspec. + ## 2.0.2 * Updated installation instructions in README. diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index f101543598b8..c5eb57c1fc6e 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_web description: Web platform implementation of video_player. repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.0.2 +version: 2.0.3 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: video_player platforms: web: pluginClass: VideoPlayerPlugin diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 87917d63d3fc..063ae82c386d 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -2,6 +2,7 @@ - Added Android native integration test support to `native-test`. - Added a new `android-lint` command to lint Android plugin native code. +- Pubspec validation now checks for `implements` in implementation packages. ## 0.5.0 diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 539b170dbea1..58aeca1447a8 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -7,6 +7,7 @@ import 'package:git/git.dart'; import 'package:platform/platform.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; +import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; @@ -65,8 +66,8 @@ class PubspecCheckCommand extends PackageLoopingCommand { @override Future runForPackage(Directory package) async { final File pubspec = package.childFile('pubspec.yaml'); - final bool passesCheck = !pubspec.existsSync() || - await _checkPubspec(pubspec, packageName: package.basename); + final bool passesCheck = + !pubspec.existsSync() || await _checkPubspec(pubspec, package: package); if (!passesCheck) { return PackageResult.fail(); } @@ -75,7 +76,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { Future _checkPubspec( File pubspecFile, { - required String packageName, + required Directory package, }) async { final String contents = pubspecFile.readAsStringSync(); final Pubspec? pubspec = _tryParsePubspec(contents); @@ -84,34 +85,43 @@ class PubspecCheckCommand extends PackageLoopingCommand { } final List pubspecLines = contents.split('\n'); - final List sectionOrder = pubspecLines.contains(' plugin:') - ? _majorPluginSections - : _majorPackageSections; + final bool isPlugin = pubspec.flutter?.containsKey('plugin') ?? false; + final List sectionOrder = + isPlugin ? _majorPluginSections : _majorPackageSections; bool passing = _checkSectionOrder(pubspecLines, sectionOrder); if (!passing) { - print('${indentation}Major sections should follow standard ' + printError('${indentation}Major sections should follow standard ' 'repository ordering:'); final String listIndentation = indentation * 2; - print('$listIndentation${sectionOrder.join('\n$listIndentation')}'); + printError('$listIndentation${sectionOrder.join('\n$listIndentation')}'); } if (pubspec.publishTo != 'none') { final List repositoryErrors = - _checkForRepositoryLinkErrors(pubspec, packageName: packageName); + _checkForRepositoryLinkErrors(pubspec, packageName: package.basename); if (repositoryErrors.isNotEmpty) { for (final String error in repositoryErrors) { - print('$indentation$error'); + printError('$indentation$error'); } passing = false; } if (!_checkIssueLink(pubspec)) { - print( + printError( '${indentation}A package should have an "issue_tracker" link to a ' 'search for open flutter/flutter bugs with the relevant label:\n' '${indentation * 2}$_expectedIssueLinkFormat'); passing = false; } + + if (isPlugin) { + final String? error = + _checkForImplementsError(pubspec, package: package); + if (error != null) { + printError('$indentation$error'); + passing = false; + } + } } return passing; @@ -168,4 +178,52 @@ class PubspecCheckCommand extends PackageLoopingCommand { .startsWith(_expectedIssueLinkFormat) == true; } + + // Validates the "implements" keyword for a plugin, returning an error + // string if there are any issues. + // + // Should only be called on plugin packages. + String? _checkForImplementsError( + Pubspec pubspec, { + required Directory package, + }) { + if (_isImplementationPackage(package)) { + final String? implements = + pubspec.flutter!['plugin']!['implements'] as String?; + final String expectedImplements = package.parent.basename; + if (implements == null) { + return 'Missing "implements: $expectedImplements" in "plugin" section.'; + } else if (implements != expectedImplements) { + return 'Expecetd "implements: $expectedImplements"; ' + 'found "implements: $implements".'; + } + } + return null; + } + + // Returns true if [packageName] appears to be an implementation package + // according to repository conventions. + bool _isImplementationPackage(Directory package) { + // An implementation package should be in a group folder... + final Directory parentDir = package.parent; + if (parentDir.path == packagesDir.path) { + return false; + } + final String packageName = package.basename; + final String parentName = parentDir.basename; + // ... whose name is a prefix of the package name. + if (!packageName.startsWith(parentName)) { + return false; + } + // A few known package names are not implementation packages; assume + // anything else is. (This is done instead of listing known implementation + // suffixes to allow for non-standard suffixes; e.g., to put several + // platforms in one package for code-sharing purposes.) + const Set nonImplementationSuffixes = { + '', // App-facing package. + '_platform_interface', // Platform interface package. + }; + final String suffix = packageName.substring(parentName.length); + return !nonImplementationSuffixes.contains(suffix); + } } diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 177ed7f25b4e..a038e0c58fb0 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -66,9 +66,13 @@ environment: '''; } - String flutterSection({bool isPlugin = false}) { - const String pluginEntry = ''' + String flutterSection({ + bool isPlugin = false, + String? implementedPackage, + }) { + final String pluginEntry = ''' plugin: +${implementedPackage == null ? '' : ' implements: $implementedPackage'} platforms: '''; return ''' @@ -177,12 +181,19 @@ ${dependenciesSection()} ${devDependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Found a "homepage" entry; only "repository" should be used.'), + ]), ); }); @@ -197,12 +208,18 @@ ${dependenciesSection()} ${devDependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Missing "repository"'), + ]), ); }); @@ -217,12 +234,19 @@ ${dependenciesSection()} ${devDependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Found a "homepage" entry; only "repository" should be used.'), + ]), ); }); @@ -237,12 +261,18 @@ ${dependenciesSection()} ${devDependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('A package should have an "issue_tracker" link'), + ]), ); }); @@ -257,12 +287,19 @@ ${devDependenciesSection()} ${environmentSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), ); }); @@ -277,12 +314,19 @@ ${dependenciesSection()} ${devDependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), ); }); @@ -297,12 +341,19 @@ ${devDependenciesSection()} ${dependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), ); }); @@ -317,12 +368,150 @@ ${flutterSection(isPlugin: true)} ${dependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), + ); + }); - await expectLater( - result, - throwsA(isA()), + test('fails when an implemenation package is missing "implements"', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_foo', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Missing "implements: plugin_a" in "plugin" section.'), + ]), + ); + }); + + test('fails when an implemenation package has the wrong "implements"', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_foo', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true, implementedPackage: 'plugin_a_foo')} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Expecetd "implements: plugin_a"; ' + 'found "implements: plugin_a_foo".'), + ]), + ); + }); + + test('passes for a correct implemenation package', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_foo', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true, implementedPackage: 'plugin_a')} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin_a_foo...'), + contains('No issues found!'), + ]), + ); + }); + + test('passes for an app-facing package without "implements"', () async { + final Directory pluginDirectory = + createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin_a/plugin_a...'), + contains('No issues found!'), + ]), + ); + }); + + test('passes for a platform interface package without "implements"', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_platform_interface', + packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_platform_interface', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin_a_platform_interface...'), + contains('No issues found!'), + ]), ); }); }); From b1fe1912e016f5566a7b4d171cb06f826bd98bbb Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 20 Aug 2021 11:50:56 -0700 Subject: [PATCH 084/123] [flutter_plugin_tools] Improve 'repository' check (#4244) Ensures that the full relative path in the 'repository' link is correct, not just the last segment. This ensure that path-level errors (e.g., linking to the group directory rather than the package itself for app-facing packages) are caught. Also fixes the errors that this improved check catches, including several cases where a previously unfederated package wasn't fixed when it was moved to a subdirectory. --- .../in_app_purchase/CHANGELOG.md | 4 ++ .../in_app_purchase/pubspec.yaml | 4 +- packages/ios_platform_images/CHANGELOG.md | 3 +- packages/ios_platform_images/pubspec.yaml | 4 +- .../quick_actions/quick_actions/CHANGELOG.md | 3 +- .../quick_actions/quick_actions/pubspec.yaml | 4 +- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/pubspec_check_command.dart | 14 +++-- .../tool/test/pubspec_check_command_test.dart | 61 +++++++++++++++++-- 9 files changed, 81 insertions(+), 17 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md index 228fcddb6370..95ba4f27d10a 100644 --- a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.8 + +* Fix repository link in pubspec.yaml. + ## 1.0.7 * Remove references to the Android V1 embedding. diff --git a/packages/in_app_purchase/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/pubspec.yaml index a37ae07baa86..8b4510b3fce4 100644 --- a/packages/in_app_purchase/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/pubspec.yaml @@ -1,8 +1,8 @@ name: in_app_purchase description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play. -repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase +repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 1.0.7 +version: 1.0.8 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/ios_platform_images/CHANGELOG.md b/packages/ios_platform_images/CHANGELOG.md index 60db21a450d8..a7270eed0576 100644 --- a/packages/ios_platform_images/CHANGELOG.md +++ b/packages/ios_platform_images/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 0.2.0+1 * Add iOS unit test target. +* Fix repository link in pubspec.yaml. ## 0.2.0 diff --git a/packages/ios_platform_images/pubspec.yaml b/packages/ios_platform_images/pubspec.yaml index e90937f4f0b5..c3938856e386 100644 --- a/packages/ios_platform_images/pubspec.yaml +++ b/packages/ios_platform_images/pubspec.yaml @@ -1,8 +1,8 @@ name: ios_platform_images description: A plugin to share images between Flutter and iOS in add-to-app setups. -repository: https://github.com/flutter/plugins/tree/master/packages/ios_platform_images/ios_platform_images +repository: https://github.com/flutter/plugins/tree/master/packages/ios_platform_images issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+ios_platform_images%22 -version: 0.2.0 +version: 0.2.0+1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md index 9087c2807061..d893b67b10dc 100644 --- a/packages/quick_actions/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 0.6.0+6 * Updated Android lint settings. +* Fix repository link in pubspec.yaml. ## 0.6.0+5 diff --git a/packages/quick_actions/quick_actions/pubspec.yaml b/packages/quick_actions/quick_actions/pubspec.yaml index e52ab515432f..c5d3fe4d4cbe 100644 --- a/packages/quick_actions/quick_actions/pubspec.yaml +++ b/packages/quick_actions/quick_actions/pubspec.yaml @@ -1,9 +1,9 @@ name: quick_actions description: Flutter plugin for creating shortcuts on home screen, also known as Quick Actions on iOS and App Shortcuts on Android. -repository: https://github.com/flutter/plugins/tree/master/packages/quick_actions +repository: https://github.com/flutter/plugins/tree/master/packages/quick_actions/quick_actions issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+quick_actions%22 -version: 0.6.0+5 +version: 0.6.0+6 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 063ae82c386d..1881d1bb6689 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -3,6 +3,7 @@ - Added Android native integration test support to `native-test`. - Added a new `android-lint` command to lint Android plugin native code. - Pubspec validation now checks for `implements` in implementation packages. +- Pubspec valitation now checks the full relative path of `repository` entries. ## 0.5.0 diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 58aeca1447a8..0a066ab72baf 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -98,7 +98,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { if (pubspec.publishTo != 'none') { final List repositoryErrors = - _checkForRepositoryLinkErrors(pubspec, packageName: package.basename); + _checkForRepositoryLinkErrors(pubspec, package: package); if (repositoryErrors.isNotEmpty) { for (final String error in repositoryErrors) { printError('$indentation$error'); @@ -154,14 +154,18 @@ class PubspecCheckCommand extends PackageLoopingCommand { List _checkForRepositoryLinkErrors( Pubspec pubspec, { - required String packageName, + required Directory package, }) { final List errorMessages = []; if (pubspec.repository == null) { errorMessages.add('Missing "repository"'); - } else if (!pubspec.repository!.path.endsWith(packageName)) { - errorMessages - .add('The "repository" link should end with the package name.'); + } else { + final String relativePackagePath = + path.relative(package.path, from: packagesDir.parent.path); + if (!pubspec.repository!.path.endsWith(relativePackagePath)) { + errorMessages + .add('The "repository" link should end with the package path.'); + } } if (pubspec.homepage != null) { diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index a038e0c58fb0..833f7b601e50 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -37,15 +37,29 @@ void main() { runner.addCommand(command); }); + /// Returns the top section of a pubspec.yaml for a package named [name], + /// for either a flutter/packages or flutter/plugins package depending on + /// the values of [isPlugin]. + /// + /// By default it will create a header that includes all of the expected + /// values, elements can be changed via arguments to create incorrect + /// entries. + /// + /// If [includeRepository] is true, by default the path in the link will + /// be "packages/[name]"; a different "packages"-relative path can be + /// provided with [repositoryPackagesDirRelativePath]. String headerSection( String name, { bool isPlugin = false, bool includeRepository = true, + String? repositoryPackagesDirRelativePath, bool includeHomepage = false, bool includeIssueTracker = true, }) { + final String repositoryPath = repositoryPackagesDirRelativePath ?? name; final String repoLink = 'https://github.com/flutter/' - '${isPlugin ? 'plugins' : 'packages'}/tree/master/packages/$name'; + '${isPlugin ? 'plugins' : 'packages'}/tree/master/' + 'packages/$repositoryPath'; final String issueTrackerLink = 'https://github.com/flutter/flutter/issues?' 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; @@ -250,6 +264,32 @@ ${devDependenciesSection()} ); }); + test('fails when repository is incorrect', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The "repository" link should end with the package path.'), + ]), + ); + }); + test('fails when issue tracker is missing', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); @@ -446,7 +486,11 @@ ${devDependenciesSection()} 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a_foo', isPlugin: true)} +${headerSection( + 'plugin_a_foo', + isPlugin: true, + repositoryPackagesDirRelativePath: 'plugin_a/plugin_a_foo', + )} ${environmentSection()} ${flutterSection(isPlugin: true, implementedPackage: 'plugin_a')} ${dependenciesSection()} @@ -470,7 +514,11 @@ ${devDependenciesSection()} createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a', isPlugin: true)} +${headerSection( + 'plugin_a', + isPlugin: true, + repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', + )} ${environmentSection()} ${flutterSection(isPlugin: true)} ${dependenciesSection()} @@ -496,7 +544,12 @@ ${devDependenciesSection()} packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a_platform_interface', isPlugin: true)} +${headerSection( + 'plugin_a_platform_interface', + isPlugin: true, + repositoryPackagesDirRelativePath: + 'plugin_a/plugin_a_platform_interface', + )} ${environmentSection()} ${flutterSection(isPlugin: true)} ${dependenciesSection()} From 6a8681e7ac18ed625ced9e92c740fb55bd739222 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Mon, 23 Aug 2021 10:57:03 +0200 Subject: [PATCH 085/123] [image_picker] Fix pickImage not returning on iOS when dismissing the PHPicker view by swiping down. (#4228) --- .../image_picker/image_picker/CHANGELOG.md | 3 ++- .../ios/Classes/FLTImagePickerPlugin.m | 24 ++++++++++++++----- .../image_picker/image_picker/pubspec.yaml | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 4f21ed3cc398..a9255976c526 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.8.3+3 +* Fix pickImage not returning a value on iOS when dismissing PHPicker sheet by swiping. * Updated Android lint settings. ## 0.8.3+2 diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m index 4084ae65b5e0..cf3103195482 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m @@ -18,7 +18,8 @@ @interface FLTImagePickerPlugin () + PHPickerViewControllerDelegate, + UIAdaptivePresentationControllerDelegate> @property(copy, nonatomic) FlutterResult result; @@ -92,6 +93,7 @@ - (void)pickImageWithPHPicker:(int)maxImagesAllowed API_AVAILABLE(ios(14)) { _pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config]; _pickerViewController.delegate = self; + _pickerViewController.presentationController.delegate = self; self.maxImagesAllowed = maxImagesAllowed; @@ -373,18 +375,28 @@ - (NSNumber *)getDesiredImageQuality:(NSNumber *)imageQuality { return imageQuality; } +- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController { + if (self.result != nil) { + self.result(nil); + self.result = nil; + self->_arguments = nil; + } +} + - (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray *)results API_AVAILABLE(ios(14)) { [picker dismissViewControllerAnimated:YES completion:nil]; - dispatch_queue_t backgroundQueue = - dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); - dispatch_async(backgroundQueue, ^{ - if (results.count == 0) { + if (results.count == 0) { + if (self.result != nil) { self.result(nil); self.result = nil; self->_arguments = nil; - return; } + return; + } + dispatch_queue_t backgroundQueue = + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); + dispatch_async(backgroundQueue, ^{ NSNumber *maxWidth = [self->_arguments objectForKey:@"maxWidth"]; NSNumber *maxHeight = [self->_arguments objectForKey:@"maxHeight"]; NSNumber *imageQuality = [self->_arguments objectForKey:@"imageQuality"]; diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index e167d8ab891c..4becca930261 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.3+2 +version: 0.8.3+3 environment: sdk: ">=2.12.0 <3.0.0" From 0a86ac866b8b322a37d5fac36c7b15856a2b37e8 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Mon, 23 Aug 2021 18:30:36 +0200 Subject: [PATCH 086/123] [camera] android-rework part 9: Final implementation of camera class (#4059) This PR adds the final implementation for the Camera class that incorporates all the features from previous parts. --- packages/camera/camera/CHANGELOG.md | 5 +- .../io/flutter/plugins/camera/Camera.java | 1275 ++++++++--------- .../flutter/plugins/camera/CameraPlugin.java | 28 +- .../plugins/camera/CameraRegionUtils.java | 29 +- .../flutter/plugins/camera/CameraRegions.java | 76 - .../flutter/plugins/camera/CameraUtils.java | 120 +- .../flutter/plugins/camera/DartMessenger.java | 28 +- .../camera/DeviceOrientationManager.java | 200 --- .../plugins/camera/MethodCallHandlerImpl.java | 40 +- .../plugins/camera/PictureCaptureRequest.java | 96 -- .../camera/features/CameraFeatureFactory.java | 12 +- .../features/CameraFeatureFactoryImpl.java | 11 +- .../camera/features/CameraFeatures.java | 37 + .../exposurepoint/ExposurePointFeature.java | 15 +- .../focuspoint/FocusPointFeature.java | 15 +- .../noisereduction/NoiseReductionFeature.java | 19 +- .../DeviceOrientationManager.java | 192 ++- .../camera/CameraPropertiesImplTest.java | 7 +- ...s_convertPointToMeteringRectangleTest.java | 197 +++ ...aRegionUtils_getCameraBoundariesTest.java} | 120 +- .../io/flutter/plugins/camera/CameraTest.java | 843 +++++++++++ .../plugins/camera/CameraUtilsTest.java | 53 +- .../plugins/camera/CameraZoomTest.java | 18 +- .../plugins/camera/DartMessengerTest.java | 4 +- .../plugins/camera/ImageSaverTests.java | 6 +- .../camera/PictureCaptureRequestTest.java | 152 -- .../autofocus/AutoFocusFeatureTest.java | 24 +- .../features/autofocus/FocusModeTest.java | 6 +- .../exposurelock/ExposureLockFeatureTest.java | 14 +- .../exposurelock/ExposureModeTest.java | 6 +- .../ExposureOffsetFeatureTest.java | 13 +- .../ExposurePointFeatureTest.java | 121 +- .../features/flash/FlashFeatureTest.java | 22 +- .../focuspoint/FocusPointFeatureTest.java | 119 +- .../fpsrange/FpsRangeFeaturePixel4aTest.java | 2 +- .../fpsrange/FpsRangeFeatureTest.java | 12 +- .../NoiseReductionFeatureTest.java | 25 +- .../resolution/ResolutionFeatureTest.java | 22 +- .../DeviceOrientationManagerTest.java | 115 +- .../SensorOrientationFeatureTest.java | 17 +- .../zoomlevel/ZoomLevelFeatureTest.java | 18 +- .../features/zoomlevel/ZoomUtilsTest.java | 8 +- .../media/MediaRecorderBuilderTest.java | 4 +- .../camera/types/ExposureModeTest.java | 6 +- .../plugins/camera/types/FlashModeTest.java | 6 +- .../plugins/camera/types/FocusModeTest.java | 6 +- .../plugins/camera/utils/TestUtils.java | 10 + .../camera/lib/src/camera_controller.dart | 2 +- .../camera/camera/lib/src/camera_preview.dart | 4 +- packages/camera/camera/pubspec.yaml | 3 +- .../camera/test/camera_preview_test.dart | 4 +- .../lib/src/events/device_event.dart | 3 +- .../platform_interface/camera_platform.dart | 3 +- 53 files changed, 2291 insertions(+), 1902 deletions(-) delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_convertPointToMeteringRectangleTest.java rename packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/{CameraRegionUtilsTest.java => CameraRegionUtils_getCameraBoundariesTest.java} (61%) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java delete mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 694898092d7a..68188d6510ff 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,5 +1,8 @@ -## NEXT +## 0.9.0 +* Complete rewrite of Android plugin to fix many capture, focus, flash, orientation and exposure issues. +* Fixed crash when opening front-facing cameras on some legacy android devices like Sony XZ. +* Android Flash mode works with full precapture sequence. * Updated Android lint settings. ## 0.8.1+7 diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 4c1370f2f3cb..4724d22a1bcd 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -4,27 +4,19 @@ package io.flutter.plugins.camera; -import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; - import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.graphics.ImageFormat; -import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; -import android.hardware.camera2.CameraCaptureSession.CaptureCallback; -import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; -import android.hardware.camera2.CaptureFailure; import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.CaptureResult; import android.hardware.camera2.TotalCaptureResult; -import android.hardware.camera2.params.MeteringRectangle; import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; import android.media.CamcorderProfile; @@ -35,27 +27,43 @@ import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Handler; +import android.os.HandlerThread; import android.os.Looper; -import android.os.SystemClock; import android.util.Log; -import android.util.Range; -import android.util.Rational; import android.util.Size; +import android.view.Display; import android.view.Surface; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugins.camera.PictureCaptureRequest.State; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.features.CameraFeatureFactory; +import io.flutter.plugins.camera.features.CameraFeatures; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; +import io.flutter.plugins.camera.features.flash.FlashMode; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; import io.flutter.plugins.camera.media.MediaRecorderBuilder; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FlashMode; -import io.flutter.plugins.camera.types.FocusMode; -import io.flutter.plugins.camera.types.ResolutionPreset; +import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; import io.flutter.view.TextureRegistry.SurfaceTextureEntry; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -71,151 +79,173 @@ interface ErrorCallback { void onError(String errorCode, String errorMessage); } -public class Camera { +class Camera + implements CameraCaptureCallback.CameraCaptureStateListener, + ImageReader.OnImageAvailableListener, + LifecycleObserver { private static final String TAG = "Camera"; - /** Timeout for the pre-capture sequence. */ - private static final long PRECAPTURE_TIMEOUT_MS = 1000; + private static final HashMap supportedImageFormats; + + // Current supported outputs. + static { + supportedImageFormats = new HashMap<>(); + supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); + supportedImageFormats.put("jpeg", ImageFormat.JPEG); + } + + /** + * Holds all of the camera features/settings and will be used to update the request builder when + * one changes. + */ + private final CameraFeatures cameraFeatures; private final SurfaceTextureEntry flutterTexture; - private final CameraManager cameraManager; - private final DeviceOrientationManager deviceOrientationListener; - private final boolean isFrontFacing; - private final int sensorOrientation; - private final String cameraName; - private final Size captureSize; - private final Size previewSize; private final boolean enableAudio; private final Context applicationContext; - private final CamcorderProfile recordingProfile; private final DartMessenger dartMessenger; - private final CameraZoom cameraZoom; - private final CameraCharacteristics cameraCharacteristics; + private final CameraProperties cameraProperties; + private final CameraFeatureFactory cameraFeatureFactory; + private final Activity activity; + /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ + private final CameraCaptureCallback cameraCaptureCallback; + /** A {@link Handler} for running tasks in the background. */ + private Handler backgroundHandler; + + /** An additional thread for running tasks that shouldn't block the UI. */ + private HandlerThread backgroundHandlerThread; private CameraDevice cameraDevice; - private CameraCaptureSession cameraCaptureSession; + private CameraCaptureSession captureSession; private ImageReader pictureImageReader; private ImageReader imageStreamReader; - private CaptureRequest.Builder captureRequestBuilder; + /** {@link CaptureRequest.Builder} for the camera preview */ + private CaptureRequest.Builder previewRequestBuilder; + private MediaRecorder mediaRecorder; + /** True when recording video. */ private boolean recordingVideo; - private File videoRecordingFile; - private FlashMode flashMode; - private ExposureMode exposureMode; - private FocusMode focusMode; - private PictureCaptureRequest pictureCaptureRequest; - private CameraRegions cameraRegions; - private int exposureOffset; - private boolean useAutoFocus = true; - private Range fpsRange; - private PlatformChannel.DeviceOrientation lockedCaptureOrientation; - private long preCaptureStartTime; - private static final HashMap supportedImageFormats; - // Current supported outputs - static { - supportedImageFormats = new HashMap<>(); - supportedImageFormats.put("yuv420", 35); - supportedImageFormats.put("jpeg", 256); - } + private File captureFile; + + /** Holds the current capture timeouts */ + private CaptureTimeoutsWrapper captureTimeouts; + + private MethodChannel.Result flutterResult; public Camera( final Activity activity, final SurfaceTextureEntry flutterTexture, + final CameraFeatureFactory cameraFeatureFactory, final DartMessenger dartMessenger, - final String cameraName, - final String resolutionPreset, - final boolean enableAudio) - throws CameraAccessException { + final CameraProperties cameraProperties, + final ResolutionPreset resolutionPreset, + final boolean enableAudio) { + if (activity == null) { throw new IllegalStateException("No activity available!"); } - this.cameraName = cameraName; + this.activity = activity; this.enableAudio = enableAudio; this.flutterTexture = flutterTexture; this.dartMessenger = dartMessenger; - this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); this.applicationContext = activity.getApplicationContext(); - this.flashMode = FlashMode.auto; - this.exposureMode = ExposureMode.auto; - this.focusMode = FocusMode.auto; - this.exposureOffset = 0; - - cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); - initFps(cameraCharacteristics); - sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - isFrontFacing = - cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) - == CameraMetadata.LENS_FACING_FRONT; - ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); - recordingProfile = - CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - previewSize = computeBestPreviewSize(cameraName, preset); - cameraZoom = - new CameraZoom( - cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), - cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); - - deviceOrientationListener = - new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); - deviceOrientationListener.start(); + this.cameraProperties = cameraProperties; + this.cameraFeatureFactory = cameraFeatureFactory; + this.cameraFeatures = + CameraFeatures.init( + cameraFeatureFactory, cameraProperties, activity, dartMessenger, resolutionPreset); + + // Create capture callback. + captureTimeouts = new CaptureTimeoutsWrapper(3000, 3000); + cameraCaptureCallback = CameraCaptureCallback.create(this, captureTimeouts); + + startBackgroundThread(); } - private void initFps(CameraCharacteristics cameraCharacteristics) { - try { - Range[] ranges = - cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - if (ranges != null) { - for (Range range : ranges) { - int upper = range.getUpper(); - Log.i("Camera", "[FPS Range Available] is:" + range); - if (upper >= 10) { - if (fpsRange == null || upper > fpsRange.getUpper()) { - fpsRange = range; - } - } - } - } - } catch (Exception e) { - e.printStackTrace(); + @Override + public void onConverged() { + takePictureAfterPrecapture(); + } + + @Override + public void onPrecapture() { + runPrecaptureSequence(); + } + + /** + * Updates the builder settings with all of the available features. + * + * @param requestBuilder request builder to update. + */ + private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { + for (CameraFeature feature : cameraFeatures.getAllFeatures()) { + Log.d(TAG, "Updating builder with feature: " + feature.getDebugName()); + feature.updateBuilder(requestBuilder); } - Log.i("Camera", "[FPS Range] is:" + fpsRange); } private void prepareMediaRecorder(String outputFilePath) throws IOException { + Log.i(TAG, "prepareMediaRecorder"); + if (mediaRecorder != null) { mediaRecorder.release(); } + final PlatformChannel.DeviceOrientation lockedOrientation = + ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) + .getLockedCaptureOrientation(); + mediaRecorder = - new MediaRecorderBuilder(recordingProfile, outputFilePath) + new MediaRecorderBuilder(getRecordingProfile(), outputFilePath) .setEnableAudio(enableAudio) .setMediaOrientation( - lockedCaptureOrientation == null - ? deviceOrientationListener.getMediaOrientation() - : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) + lockedOrientation == null + ? getDeviceOrientationManager().getVideoOrientation() + : getDeviceOrientationManager().getVideoOrientation(lockedOrientation)) .build(); } @SuppressLint("MissingPermission") public void open(String imageFormatGroup) throws CameraAccessException { + final ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); + + if (!resolutionFeature.checkIsSupported()) { + // Tell the user that the camera they are trying to open is not supported, + // as its {@link android.media.CamcorderProfile} cannot be fetched due to the name + // not being a valid parsable integer. + dartMessenger.sendCameraErrorEvent( + "Camera with name \"" + + cameraProperties.getCameraName() + + "\" is not supported by this plugin."); + return; + } + + // Always capture using JPEG format. pictureImageReader = ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + resolutionFeature.getCaptureSize().getWidth(), + resolutionFeature.getCaptureSize().getHeight(), + ImageFormat.JPEG, + 1); + // For image streaming, use the provided image format or fall back to YUV420. Integer imageFormat = supportedImageFormats.get(imageFormatGroup); if (imageFormat == null) { Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); imageFormat = ImageFormat.YUV_420_888; } - - // Used to steam image byte data to dart side. imageStreamReader = - ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); + ImageReader.newInstance( + resolutionFeature.getPreviewSize().getWidth(), + resolutionFeature.getPreviewSize().getHeight(), + imageFormat, + 1); + // Open the camera. + CameraManager cameraManager = CameraUtils.getCameraManager(activity); cameraManager.openCamera( - cameraName, + cameraProperties.getCameraName(), new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice device) { @@ -223,12 +253,12 @@ public void onOpened(@NonNull CameraDevice device) { try { startPreview(); dartMessenger.sendCameraInitializedEvent( - previewSize.getWidth(), - previewSize.getHeight(), - exposureMode, - focusMode, - isExposurePointSupported(), - isFocusPointSupported()); + resolutionFeature.getPreviewSize().getWidth(), + resolutionFeature.getPreviewSize().getHeight(), + cameraFeatures.getExposureLock().getValue(), + cameraFeatures.getAutoFocus().getValue(), + cameraFeatures.getExposurePoint().checkIsSupported(), + cameraFeatures.getFocusPoint().checkIsSupported()); } catch (CameraAccessException e) { dartMessenger.sendCameraErrorEvent(e.getMessage()); close(); @@ -237,18 +267,24 @@ public void onOpened(@NonNull CameraDevice device) { @Override public void onClosed(@NonNull CameraDevice camera) { + Log.i(TAG, "open | onClosed"); + dartMessenger.sendCameraClosingEvent(); super.onClosed(camera); } @Override public void onDisconnected(@NonNull CameraDevice cameraDevice) { + Log.i(TAG, "open | onDisconnected"); + close(); dartMessenger.sendCameraErrorEvent("The camera was disconnected."); } @Override public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + Log.i(TAG, "open | onError"); + close(); String errorDescription; switch (errorCode) { @@ -273,7 +309,7 @@ public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { dartMessenger.sendCameraErrorEvent(errorDescription); } }, - null); + backgroundHandler); } private void createCaptureSession(int templateType, Surface... surfaces) @@ -288,39 +324,45 @@ private void createCaptureSession( closeCaptureSession(); // Create a new capture builder. - captureRequestBuilder = cameraDevice.createCaptureRequest(templateType); + previewRequestBuilder = cameraDevice.createCaptureRequest(templateType); - // Build Flutter surface to render to + // Build Flutter surface to render to. + ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + surfaceTexture.setDefaultBufferSize( + resolutionFeature.getPreviewSize().getWidth(), + resolutionFeature.getPreviewSize().getHeight()); Surface flutterSurface = new Surface(surfaceTexture); - captureRequestBuilder.addTarget(flutterSurface); + previewRequestBuilder.addTarget(flutterSurface); List remainingSurfaces = Arrays.asList(surfaces); if (templateType != CameraDevice.TEMPLATE_PREVIEW) { // If it is not preview mode, add all surfaces as targets. for (Surface surface : remainingSurfaces) { - captureRequestBuilder.addTarget(surface); + previewRequestBuilder.addTarget(surface); } } - cameraRegions = new CameraRegions(getRegionBoundaries()); + // Update camera regions. + Size cameraBoundaries = + CameraRegionUtils.getCameraBoundaries(cameraProperties, previewRequestBuilder); + cameraFeatures.getExposurePoint().setCameraBoundaries(cameraBoundaries); + cameraFeatures.getFocusPoint().setCameraBoundaries(cameraBoundaries); - // Prepare the callback + // Prepare the callback. CameraCaptureSession.StateCallback callback = new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { + // Camera was already closed. if (cameraDevice == null) { dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); return; } - cameraCaptureSession = session; + captureSession = session; - updateFpsRange(); - updateFocus(focusMode); - updateFlash(flashMode); - updateExposure(exposureMode); + Log.i(TAG, "Updating builder settings"); + updateBuilderSettings(previewRequestBuilder); refreshPreviewCaptureSession( onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); @@ -332,9 +374,9 @@ public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession } }; - // Start the session + // Start the session. if (VERSION.SDK_INT >= VERSION_CODES.P) { - // Collect all surfaces we want to render to. + // Collect all surfaces to render to. List configs = new ArrayList<>(); configs.add(new OutputConfiguration(flutterSurface)); for (Surface surface : remainingSurfaces) { @@ -342,7 +384,7 @@ public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession } createCaptureSessionWithSessionConfig(configs, callback); } else { - // Collect all surfaces we want to render to. + // Collect all surfaces to render to. List surfaceList = new ArrayList<>(); surfaceList.add(flutterSurface); surfaceList.addAll(remainingSurfaces); @@ -367,276 +409,273 @@ private void createCaptureSessionWithSessionConfig( private void createCaptureSession( List surfaces, CameraCaptureSession.StateCallback callback) throws CameraAccessException { - cameraDevice.createCaptureSession(surfaces, callback, null); + cameraDevice.createCaptureSession(surfaces, callback, backgroundHandler); } + // Send a repeating request to refresh capture session. private void refreshPreviewCaptureSession( @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { - if (cameraCaptureSession == null) { + if (captureSession == null) { + Log.i( + TAG, + "[refreshPreviewCaptureSession] captureSession not yet initialized, " + + "skipping preview capture session refresh."); return; } try { - cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), - pictureCaptureCallback, - new Handler(Looper.getMainLooper())); + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); if (onSuccessCallback != null) { onSuccessCallback.run(); } - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - onErrorCallback.onError("cameraAccess", e.getMessage()); - } - } - private void writeToFile(ByteBuffer buffer, File file) throws IOException { - try (FileOutputStream outputStream = new FileOutputStream(file)) { - while (0 < buffer.remaining()) { - outputStream.getChannel().write(buffer); - } + } catch (CameraAccessException e) { + onErrorCallback.onError("cameraAccess", e.getMessage()); } } public void takePicture(@NonNull final Result result) { - // Only take 1 picture at a time - if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { + // Only take one picture at a time. + if (cameraCaptureCallback.getCameraState() != CameraState.STATE_PREVIEW) { result.error("captureAlreadyActive", "Picture is currently already being captured", null); return; } - // Store the result - this.pictureCaptureRequest = new PictureCaptureRequest(result); - // Create temporary file + flutterResult = result; + + // Create temporary file. final File outputDir = applicationContext.getCacheDir(); - final File file; try { - file = File.createTempFile("CAP", ".jpg", outputDir); + captureFile = File.createTempFile("CAP", ".jpg", outputDir); + captureTimeouts.reset(); } catch (IOException | SecurityException e) { - pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); + dartMessenger.error(flutterResult, "cannotCreateFile", e.getMessage(), null); return; } - // Listen for picture being taken - pictureImageReader.setOnImageAvailableListener( - reader -> { - try (Image image = reader.acquireLatestImage()) { - ByteBuffer buffer = image.getPlanes()[0].getBuffer(); - writeToFile(buffer, file); - pictureCaptureRequest.finish(file.getAbsolutePath()); - } catch (IOException e) { - pictureCaptureRequest.error("IOError", "Failed saving image", null); - } - }, - null); + // Listen for picture being taken. + pictureImageReader.setOnImageAvailableListener(this, backgroundHandler); - if (useAutoFocus) { + final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus(); + final boolean isAutoFocusSupported = autoFocusFeature.checkIsSupported(); + if (isAutoFocusSupported && autoFocusFeature.getValue() == FocusMode.auto) { runPictureAutoFocus(); } else { - runPicturePreCapture(); + runPrecaptureSequence(); } } - private final CameraCaptureSession.CaptureCallback pictureCaptureCallback = - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - processCapture(result); - } + /** + * Run the precapture sequence for capturing a still image. This method should be called when a + * response is received in {@link #cameraCaptureCallback} from lockFocus(). + */ + private void runPrecaptureSequence() { + Log.i(TAG, "runPrecaptureSequence"); + try { + // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE_START. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + captureSession.capture( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + + // Repeating request to refresh preview session. + refreshPreviewCaptureSession( + null, + (code, message) -> dartMessenger.error(flutterResult, "cameraAccess", message, null)); - @Override - public void onCaptureProgressed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - processCapture(partialResult); - } + // Start precapture. + cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_PRECAPTURE_START); - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (pictureCaptureRequest == null || pictureCaptureRequest.isFinished()) { - return; - } - String reason; - boolean fatalFailure = false; - switch (failure.getReason()) { - case CaptureFailure.REASON_ERROR: - reason = "An error happened in the framework"; - break; - case CaptureFailure.REASON_FLUSHED: - reason = "The capture has failed due to an abortCaptures() call"; - fatalFailure = true; - break; - default: - reason = "Unknown reason"; - } - Log.w("Camera", "pictureCaptureCallback.onCaptureFailed(): " + reason); - if (fatalFailure) pictureCaptureRequest.error("captureFailure", reason, null); - } - - private void processCapture(CaptureResult result) { - if (pictureCaptureRequest == null) { - return; - } + previewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); - Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - switch (pictureCaptureRequest.getState()) { - case focusing: - if (afState == null) { - return; - } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED - || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - // Some devices might return null here, in which case we will also continue. - if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { - runPictureCapture(); - } else { - runPicturePreCapture(); - } - } - break; - case preCapture: - // Some devices might return null here, in which case we will also continue. - if (aeState == null - || aeState == CaptureRequest.CONTROL_AE_STATE_PRECAPTURE - || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED - || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { - pictureCaptureRequest.setState(State.waitingPreCaptureReady); - setPreCaptureStartTime(); - } - break; - case waitingPreCaptureReady: - if (aeState == null || aeState != CaptureRequest.CONTROL_AE_STATE_PRECAPTURE) { - runPictureCapture(); - } else { - if (hitPreCaptureTimeout()) { - unlockAutoFocus(); - } - } - } - } - }; + // Trigger one capture to start AE sequence. + captureSession.capture( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); - private void runPictureAutoFocus() { - assert (pictureCaptureRequest != null); - - pictureCaptureRequest.setState(PictureCaptureRequest.State.focusing); - lockAutoFocus(pictureCaptureCallback); + } catch (CameraAccessException e) { + e.printStackTrace(); + } } - private void runPicturePreCapture() { - assert (pictureCaptureRequest != null); - pictureCaptureRequest.setState(PictureCaptureRequest.State.preCapture); + /** + * Capture a still picture. This method should be called when a response is received {@link + * #cameraCaptureCallback} from both lockFocus(). + */ + private void takePictureAfterPrecapture() { + Log.i(TAG, "captureStillPicture"); + cameraCaptureCallback.setCameraState(CameraState.STATE_CAPTURING); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + if (cameraDevice == null) { + return; + } + // This is the CaptureRequest.Builder that is used to take a picture. + CaptureRequest.Builder stillBuilder; + try { + stillBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + } catch (CameraAccessException e) { + dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); + return; + } + stillBuilder.addTarget(pictureImageReader.getSurface()); + + // Zoom. + stillBuilder.set( + CaptureRequest.SCALER_CROP_REGION, + previewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + + // Have all features update the builder. + updateBuilderSettings(stillBuilder); + + // Orientation. + final PlatformChannel.DeviceOrientation lockedOrientation = + ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) + .getLockedCaptureOrientation(); + stillBuilder.set( + CaptureRequest.JPEG_ORIENTATION, + lockedOrientation == null + ? getDeviceOrientationManager().getPhotoOrientation() + : getDeviceOrientationManager().getPhotoOrientation(lockedOrientation)); + + CameraCaptureSession.CaptureCallback captureCallback = + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + unlockAutoFocus(); + } + }; - refreshPreviewCaptureSession( - () -> - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE), - (code, message) -> pictureCaptureRequest.error(code, message, null)); + try { + captureSession.stopRepeating(); + captureSession.abortCaptures(); + Log.i(TAG, "sending capture request"); + captureSession.capture(stillBuilder.build(), captureCallback, backgroundHandler); + } catch (CameraAccessException e) { + dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); + } + } + + @SuppressWarnings("deprecation") + private Display getDefaultDisplay() { + return activity.getWindowManager().getDefaultDisplay(); } - private void runPictureCapture() { - assert (pictureCaptureRequest != null); - pictureCaptureRequest.setState(PictureCaptureRequest.State.capturing); + /** Starts a background thread and its {@link Handler}. */ + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + public void startBackgroundThread() { + backgroundHandlerThread = new HandlerThread("CameraBackground"); try { - final CaptureRequest.Builder captureBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - captureBuilder.addTarget(pictureImageReader.getSurface()); - captureBuilder.set( - CaptureRequest.SCALER_CROP_REGION, - captureRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); - captureBuilder.set( - CaptureRequest.JPEG_ORIENTATION, - lockedCaptureOrientation == null - ? deviceOrientationListener.getMediaOrientation() - : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)); - - switch (flashMode) { - case off: - captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case auto: - captureBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); - break; - case always: - default: - captureBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); - break; + backgroundHandlerThread.start(); + } catch (IllegalThreadStateException e) { + // Ignore exception in case the thread has already started. + } + backgroundHandler = new Handler(backgroundHandlerThread.getLooper()); + } + + /** Stops the background thread and its {@link Handler}. */ + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + public void stopBackgroundThread() { + if (backgroundHandlerThread != null) { + backgroundHandlerThread.quitSafely(); + try { + backgroundHandlerThread.join(); + } catch (InterruptedException e) { + dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); } - cameraCaptureSession.stopRepeating(); - cameraCaptureSession.capture( - captureBuilder.build(), - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - unlockAutoFocus(); - } - }, - null); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } + backgroundHandlerThread = null; + backgroundHandler = null; + } + + /** Start capturing a picture, doing autofocus first. */ + private void runPictureAutoFocus() { + Log.i(TAG, "runPictureAutoFocus"); + + cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); + lockAutoFocus(); } - private void lockAutoFocus(CaptureCallback callback) { - captureRequestBuilder.set( + private void lockAutoFocus() { + Log.i(TAG, "lockAutoFocus"); + if (captureSession == null) { + Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); + return; + } + + // Trigger AF to start. + previewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + try { + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + } } + /** Cancel and reset auto focus state and refresh the preview session. */ private void unlockAutoFocus() { - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - updateFocus(focusMode); + Log.i(TAG, "unlockAutoFocus"); + if (captureSession == null) { + Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); + return; + } try { - cameraCaptureSession.capture(captureRequestBuilder.build(), null, null); - } catch (CameraAccessException ignored) { + // Cancel existing AF state. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); + + // Set AF state to idle again. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + return; } - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); refreshPreviewCaptureSession( null, - (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); + (errorCode, errorMessage) -> + dartMessenger.error(flutterResult, errorCode, errorMessage, null)); } - public void startVideoRecording(Result result) { + public void startVideoRecording(@NonNull Result result) { final File outputDir = applicationContext.getCacheDir(); try { - videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); + captureFile = File.createTempFile("REC", ".mp4", outputDir); } catch (IOException | SecurityException e) { result.error("cannotCreateFile", e.getMessage(), null); return; } - try { - prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); - recordingVideo = true; + prepareMediaRecorder(captureFile.getAbsolutePath()); + } catch (IOException e) { + recordingVideo = false; + captureFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + // Re-create autofocus feature so it's using video focus mode now. + cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true)); + recordingVideo = true; + try { createCaptureSession( CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); result.success(null); - } catch (CameraAccessException | IOException e) { + } catch (CameraAccessException e) { recordingVideo = false; - videoRecordingFile = null; + captureFile = null; result.error("videoRecordingFailed", e.getMessage(), null); } } @@ -646,24 +685,25 @@ public void stopVideoRecording(@NonNull final Result result) { result.success(null); return; } - + // Re-create autofocus feature so it's using continuous capture focus mode now. + cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); + recordingVideo = false; + try { + captureSession.abortCaptures(); + mediaRecorder.stop(); + } catch (CameraAccessException | IllegalStateException e) { + // Ignore exceptions and try to continue (changes are camera session already aborted capture). + } + mediaRecorder.reset(); try { - recordingVideo = false; - - try { - cameraCaptureSession.abortCaptures(); - mediaRecorder.stop(); - } catch (CameraAccessException | IllegalStateException e) { - // Ignore exceptions and try to continue (changes are camera session already aborted capture) - } - - mediaRecorder.reset(); startPreview(); - result.success(videoRecordingFile.getAbsolutePath()); - videoRecordingFile = null; } catch (CameraAccessException | IllegalStateException e) { result.error("videoRecordingFailed", e.getMessage(), null); + return; } + result.success(captureFile.getAbsolutePath()); + captureFile = null; } public void pauseVideoRecording(@NonNull final Result result) { @@ -709,259 +749,185 @@ public void resumeVideoRecording(@NonNull final Result result) { result.success(null); } - public void setFlashMode(@NonNull final Result result, FlashMode mode) - throws CameraAccessException { - // Get the flash availability - Boolean flashAvailable = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - - // Check if flash is available. - if (flashAvailable == null || !flashAvailable) { - result.error("setFlashModeFailed", "Device does not have flash capabilities", null); - return; - } + /** + * Method handler for setting new flash modes. + * + * @param result Flutter result. + * @param newMode new mode. + */ + public void setFlashMode(@NonNull final Result result, @NonNull FlashMode newMode) { + // Save the new flash mode setting. + final FlashFeature flashFeature = cameraFeatures.getFlash(); + flashFeature.setValue(newMode); + flashFeature.updateBuilder(previewRequestBuilder); - // If switching directly from torch to auto or on, make sure we turn off the torch. - if (flashMode == FlashMode.torch && mode != FlashMode.torch && mode != FlashMode.off) { - updateFlash(FlashMode.off); - - this.cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), - new CaptureCallback() { - private boolean isFinished = false; - - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult captureResult) { - if (isFinished) { - return; - } + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } - updateFlash(mode); - refreshPreviewCaptureSession( - () -> { - result.success(null); - isFinished = true; - }, - (code, message) -> - result.error("setFlashModeFailed", "Could not set flash mode.", null)); - } + /** + * Method handler for setting new exposure modes. + * + * @param result Flutter result. + * @param newMode new mode. + */ + public void setExposureMode(@NonNull final Result result, @NonNull ExposureMode newMode) { + final ExposureLockFeature exposureLockFeature = cameraFeatures.getExposureLock(); + exposureLockFeature.setValue(newMode); + exposureLockFeature.updateBuilder(previewRequestBuilder); - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (isFinished) { - return; - } + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposureModeFailed", "Could not set exposure mode.", null)); + } - result.error("setFlashModeFailed", "Could not set flash mode.", null); - isFinished = true; - } - }, - null); - } else { - updateFlash(mode); + /** + * Sets new exposure point from dart. + * + * @param result Flutter result. + * @param point The exposure point. + */ + public void setExposurePoint(@NonNull final Result result, @Nullable Point point) { + final ExposurePointFeature exposurePointFeature = cameraFeatures.getExposurePoint(); + exposurePointFeature.setValue(point); + exposurePointFeature.updateBuilder(previewRequestBuilder); - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); - } + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposurePointFailed", "Could not set exposure point.", null)); } - public void setExposureMode(@NonNull final Result result, ExposureMode mode) - throws CameraAccessException { - updateExposure(mode); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); - result.success(null); + /** Return the max exposure offset value supported by the camera to dart. */ + public double getMaxExposureOffset() { + return cameraFeatures.getExposureOffset().getMaxExposureOffset(); } - public void setExposurePoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { - // Check if exposure point functionality is available. - if (!isExposurePointSupported()) { - result.error( - "setExposurePointFailed", "Device does not have exposure point capabilities", null); - return; - } - // Check if the current region boundaries are known - if (cameraRegions.getMaxBoundaries() == null) { - result.error("setExposurePointFailed", "Could not determine max region boundaries", null); - return; - } - // Set the metering rectangle - if (x == null || y == null) cameraRegions.resetAutoExposureMeteringRectangle(); - else cameraRegions.setAutoExposureMeteringRectangleFromPoint(y, 1 - x); - // Apply it - updateExposure(exposureMode); - refreshPreviewCaptureSession( - () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); + /** Return the min exposure offset value supported by the camera to dart. */ + public double getMinExposureOffset() { + return cameraFeatures.getExposureOffset().getMinExposureOffset(); } - public void setFocusMode(@NonNull final Result result, FocusMode mode) - throws CameraAccessException { - this.focusMode = mode; - - updateFocus(mode); + /** Return the exposure offset step size to dart. */ + public double getExposureOffsetStepSize() { + return cameraFeatures.getExposureOffset().getExposureOffsetStepSize(); + } - switch (mode) { - case auto: - refreshPreviewCaptureSession( - null, (code, message) -> result.error("setFocusMode", message, null)); - break; + /** + * Sets new focus mode from dart. + * + * @param result Flutter result. + * @param newMode New mode. + */ + public void setFocusMode(final Result result, @NonNull FocusMode newMode) { + final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus(); + autoFocusFeature.setValue(newMode); + autoFocusFeature.updateBuilder(previewRequestBuilder); + + /* + * For focus mode an extra step of actually locking/unlocking the + * focus has to be done, in order to ensure it goes into the correct state. + */ + switch (newMode) { case locked: - lockAutoFocus( - new CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - unlockAutoFocus(); - } - }); - break; - } - result.success(null); - } + // Perform a single focus trigger. + lockAutoFocus(); + if (captureSession == null) { + Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); + return; + } - public void setFocusPoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { - // Check if focus point functionality is available. - if (!isFocusPointSupported()) { - result.error("setFocusPointFailed", "Device does not have focus point capabilities", null); - return; - } + // Set AF state to idle again. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); - // Check if the current region boundaries are known - if (cameraRegions.getMaxBoundaries() == null) { - result.error("setFocusPointFailed", "Could not determine max region boundaries", null); - return; + try { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + if (result != null) { + result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + } + return; + } + break; + case auto: + // Cancel current AF trigger and set AF to idle again. + unlockAutoFocus(); + break; } - // Set the metering rectangle - if (x == null || y == null) { - cameraRegions.resetAutoFocusMeteringRectangle(); - } else { - cameraRegions.setAutoFocusMeteringRectangleFromPoint(y, 1 - x); + if (result != null) { + result.success(null); } - - // Apply the new metering rectangle - setFocusMode(result, focusMode); } - @TargetApi(VERSION_CODES.P) - private boolean supportsDistortionCorrection() throws CameraAccessException { - int[] availableDistortionCorrectionModes = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); - if (availableDistortionCorrectionModes == null) availableDistortionCorrectionModes = new int[0]; - long nonOffModesSupported = - Arrays.stream(availableDistortionCorrectionModes) - .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) - .count(); - return nonOffModesSupported > 0; - } - - private Size getRegionBoundaries() throws CameraAccessException { - // No distortion correction support - if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { - return cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); - } - // Get the current distortion correction mode - Integer distortionCorrectionMode = - captureRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); - // Return the correct boundaries depending on the mode - android.graphics.Rect rect; - if (distortionCorrectionMode == null - || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); - } else { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); - } - return rect == null ? null : new Size(rect.width(), rect.height()); - } + /** + * Sets new focus point from dart. + * + * @param result Flutter result. + * @param point the new coordinates. + */ + public void setFocusPoint(@NonNull final Result result, @Nullable Point point) { + final FocusPointFeature focusPointFeature = cameraFeatures.getFocusPoint(); + focusPointFeature.setValue(point); + focusPointFeature.updateBuilder(previewRequestBuilder); - private boolean isExposurePointSupported() throws CameraAccessException { - Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); - return supportedRegions != null && supportedRegions > 0; - } + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFocusPointFailed", "Could not set focus point.", null)); - private boolean isFocusPointSupported() throws CameraAccessException { - Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); - return supportedRegions != null && supportedRegions > 0; + this.setFocusMode(null, cameraFeatures.getAutoFocus().getValue()); } - public double getMinExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - double minStepped = range == null ? 0 : range.getLower(); - double stepSize = getExposureOffsetStepSize(); - return minStepped * stepSize; - } + /** + * Sets a new exposure offset from dart. From dart the offset comes as a double, like +1.3 or + * -1.3. + * + * @param result flutter result. + * @param offset new value. + */ + public void setExposureOffset(@NonNull final Result result, double offset) { + final ExposureOffsetFeature exposureOffsetFeature = cameraFeatures.getExposureOffset(); + exposureOffsetFeature.setValue(offset); + exposureOffsetFeature.updateBuilder(previewRequestBuilder); - public double getMaxExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - double maxStepped = range == null ? 0 : range.getUpper(); - double stepSize = getExposureOffsetStepSize(); - return maxStepped * stepSize; + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposureOffsetFailed", "Could not set exposure offset.", null)); } - public double getExposureOffsetStepSize() throws CameraAccessException { - Rational stepSize = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); - return stepSize == null ? 0.0 : stepSize.doubleValue(); + public float getMaxZoomLevel() { + return cameraFeatures.getZoomLevel().getMaximumZoomLevel(); } - public void setExposureOffset(@NonNull final Result result, double offset) - throws CameraAccessException { - // Set the exposure offset - double stepSize = getExposureOffsetStepSize(); - exposureOffset = (int) (offset / stepSize); - // Apply it - updateExposure(exposureMode); - this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); - result.success(offset); + public float getMinZoomLevel() { + return cameraFeatures.getZoomLevel().getMinimumZoomLevel(); } - public float getMaxZoomLevel() { - return cameraZoom.maxZoom; + /** Shortcut to get current recording profile. */ + CamcorderProfile getRecordingProfile() { + return cameraFeatures.getResolution().getRecordingProfile(); } - public float getMinZoomLevel() { - return CameraZoom.DEFAULT_ZOOM_FACTOR; + /** Shortut to get deviceOrientationListener. */ + DeviceOrientationManager getDeviceOrientationManager() { + return cameraFeatures.getSensorOrientation().getDeviceOrientationManager(); } + /** + * Sets zoom level from dart. + * + * @param result Flutter result. + * @param zoom new value. + */ public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { - float maxZoom = cameraZoom.maxZoom; - float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; + final ZoomLevelFeature zoomLevel = cameraFeatures.getZoomLevel(); + float maxZoom = zoomLevel.getMaximumZoomLevel(); + float minZoom = zoomLevel.getMinimumZoomLevel(); if (zoom > maxZoom || zoom < minZoom) { String errorMessage = @@ -974,122 +940,31 @@ public void setZoomLevel(@NonNull final Result result, float zoom) throws Camera return; } - //Zoom area is calculated relative to sensor area (activeRect) - if (captureRequestBuilder != null) { - final Rect computedZoom = cameraZoom.computeZoom(zoom); - captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); - } + zoomLevel.setValue(zoom); + zoomLevel.updateBuilder(previewRequestBuilder); - result.success(null); + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setZoomLevelFailed", "Could not set zoom level.", null)); } + /** + * Lock capture orientation from dart. + * + * @param orientation new orientation. + */ public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { - this.lockedCaptureOrientation = orientation; + cameraFeatures.getSensorOrientation().lockCaptureOrientation(orientation); } + /** Unlock capture orientation from dart. */ public void unlockCaptureOrientation() { - this.lockedCaptureOrientation = null; - } - - private void updateFpsRange() { - if (fpsRange == null) { - return; - } - - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); - } - - private void updateFocus(FocusMode mode) { - if (useAutoFocus) { - int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); - // Auto focus is not supported - if (modes == null - || modes.length == 0 - || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) { - useAutoFocus = false; - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); - } else { - // Applying auto focus - switch (mode) { - case locked: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); - break; - case auto: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, - recordingVideo - ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO - : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); - default: - break; - } - MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_REGIONS, - afRect == null ? null : new MeteringRectangle[] {afRect}); - } - } else { - captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); - } - } - - private void updateExposure(ExposureMode mode) { - exposureMode = mode; - - // Applying auto exposure - MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_REGIONS, - aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); - - switch (mode) { - case locked: - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); - break; - case auto: - default: - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); - break; - } - - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); - } - - private void updateFlash(FlashMode mode) { - // Get flash - flashMode = mode; - - // Applying flash modes - switch (flashMode) { - case off: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case auto: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case always: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case torch: - default: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); - break; - } + cameraFeatures.getSensorOrientation().unlockCaptureOrientation(); } public void startPreview() throws CameraAccessException { if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; + Log.i(TAG, "startPreview"); createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); } @@ -1097,6 +972,7 @@ public void startPreview() throws CameraAccessException { public void startPreviewWithImageStream(EventChannel imageStreamChannel) throws CameraAccessException { createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); + Log.i(TAG, "startPreviewWithImageStream"); imageStreamChannel.setStreamHandler( new EventChannel.StreamHandler() { @@ -1107,15 +983,43 @@ public void onListen(Object o, EventChannel.EventSink imageStreamSink) { @Override public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, null); + imageStreamReader.setOnImageAvailableListener(null, backgroundHandler); } }); } + /** + * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a + * still image is ready to be saved. + */ + @Override + public void onImageAvailable(ImageReader reader) { + Log.i(TAG, "onImageAvailable"); + + backgroundHandler.post( + new ImageSaver( + // Use acquireNextImage since image reader is only for one image. + reader.acquireNextImage(), + captureFile, + new ImageSaver.Callback() { + @Override + public void onComplete(String absolutePath) { + dartMessenger.finish(flutterResult, absolutePath); + } + + @Override + public void onError(String errorCode, String errorMessage) { + dartMessenger.error(flutterResult, errorCode, errorMessage, null); + } + })); + cameraCaptureCallback.setCameraState(CameraState.STATE_PREVIEW); + } + private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { imageStreamReader.setOnImageAvailableListener( reader -> { - Image img = reader.acquireLatestImage(); + // Use acquireNextImage since image reader is only for one image. + Image img = reader.acquireNextImage(); if (img == null) return; List> planes = new ArrayList<>(); @@ -1139,41 +1043,24 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i imageBuffer.put("format", img.getFormat()); imageBuffer.put("planes", planes); - imageStreamSink.success(imageBuffer); + final Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> imageStreamSink.success(imageBuffer)); img.close(); }, - null); - } - - public void stopImageStream() throws CameraAccessException { - if (imageStreamReader != null) { - imageStreamReader.setOnImageAvailableListener(null, null); - } - startPreview(); - } - - /** Sets the time the pre-capture sequence started. */ - private void setPreCaptureStartTime() { - preCaptureStartTime = SystemClock.elapsedRealtime(); - } - - /** - * Check if the timeout for the pre-capture sequence has been reached. - * - * @return true if the timeout is reached; otherwise false is returned. - */ - private boolean hitPreCaptureTimeout() { - return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; + backgroundHandler); } private void closeCaptureSession() { - if (cameraCaptureSession != null) { - cameraCaptureSession.close(); - cameraCaptureSession = null; + if (captureSession != null) { + Log.i(TAG, "closeCaptureSession"); + + captureSession.close(); + captureSession = null; } } public void close() { + Log.i(TAG, "close"); closeCaptureSession(); if (cameraDevice != null) { @@ -1193,11 +1080,15 @@ public void close() { mediaRecorder.release(); mediaRecorder = null; } + + stopBackgroundThread(); } public void dispose() { + Log.i(TAG, "dispose"); + close(); flutterTexture.release(); - deviceOrientationListener.stop(); + getDeviceOrientationManager().stop(); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index 75730ab41711..ef3a2b9b5d83 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -8,9 +8,11 @@ import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; import io.flutter.view.TextureRegistry; @@ -51,7 +53,8 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra registrar.activity(), registrar.messenger(), registrar::addRequestPermissionsResultListener, - registrar.view()); + registrar.view(), + null); } @Override @@ -70,18 +73,17 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { binding.getActivity(), flutterPluginBinding.getBinaryMessenger(), binding::addRequestPermissionsResultListener, - flutterPluginBinding.getTextureRegistry()); + flutterPluginBinding.getTextureRegistry(), + FlutterLifecycleAdapter.getActivityLifecycle(binding)); } @Override public void onDetachedFromActivity() { - if (methodCallHandler == null) { - // Could be on too low of an SDK to have started listening originally. - return; + // Could be on too low of an SDK to have started listening originally. + if (methodCallHandler != null) { + methodCallHandler.stopListening(); + methodCallHandler = null; } - - methodCallHandler.stopListening(); - methodCallHandler = null; } @Override @@ -98,7 +100,8 @@ private void maybeStartListening( Activity activity, BinaryMessenger messenger, PermissionsRegistry permissionsRegistry, - TextureRegistry textureRegistry) { + TextureRegistry textureRegistry, + @Nullable Lifecycle lifecycle) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { // If the sdk is less than 21 (min sdk for Camera2) we don't register the plugin. return; @@ -106,6 +109,11 @@ private void maybeStartListening( methodCallHandler = new MethodCallHandlerImpl( - activity, messenger, new CameraPermissions(), permissionsRegistry, textureRegistry); + activity, + messenger, + new CameraPermissions(), + permissionsRegistry, + textureRegistry, + lifecycle); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java index ff8a49f1d148..951a2797d68f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java @@ -11,6 +11,7 @@ import android.util.Size; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import java.util.Arrays; /** @@ -69,11 +70,32 @@ && supportsDistortionCorrection(cameraProperties)) { * boundaries. */ public static MeteringRectangle convertPointToMeteringRectangle( - @NonNull Size boundaries, double x, double y) { + @NonNull Size boundaries, + double x, + double y, + @NonNull PlatformChannel.DeviceOrientation orientation) { assert (boundaries.getWidth() > 0 && boundaries.getHeight() > 0); assert (x >= 0 && x <= 1); assert (y >= 0 && y <= 1); - + // Rotate the coordinates to match the device orientation. + double oldX = x, oldY = y; + switch (orientation) { + case PORTRAIT_UP: // 90 ccw. + y = 1 - oldX; + x = oldY; + break; + case PORTRAIT_DOWN: // 90 cw. + x = 1 - oldY; + y = oldX; + break; + case LANDSCAPE_LEFT: + // No rotation required. + break; + case LANDSCAPE_RIGHT: // 180. + x = 1 - x; + y = 1 - y; + break; + } // Interpolate the target coordinate. int targetX = (int) Math.round(x * ((double) (boundaries.getWidth() - 1))); int targetY = (int) Math.round(y * ((double) (boundaries.getHeight() - 1))); @@ -98,7 +120,6 @@ public static MeteringRectangle convertPointToMeteringRectangle( if (targetY > maxTargetY) { targetY = maxTargetY; } - // Build the metering rectangle. return MeteringRectangleFactory.create(targetX, targetY, targetWidth, targetHeight, 1); } @@ -130,7 +151,7 @@ static class MeteringRectangleFactory { * @param width width >= 0. * @param height height >= 0. * @param meteringWeight weight between {@value MeteringRectangle#METERING_WEIGHT_MIN} and - * {@value MeteringRectangle#METERING_WEIGHT_MAX} inclusively + * {@value MeteringRectangle#METERING_WEIGHT_MAX} inclusively. * @return new instance of the {@link MeteringRectangle} class. * @throws IllegalArgumentException if any of the parameters were negative. */ diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java deleted file mode 100644 index 60c866cd82d5..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.camera; - -import android.hardware.camera2.params.MeteringRectangle; -import android.util.Size; - -public final class CameraRegions { - private MeteringRectangle aeMeteringRectangle; - private MeteringRectangle afMeteringRectangle; - private Size maxBoundaries; - - public CameraRegions(Size maxBoundaries) { - assert (maxBoundaries == null || maxBoundaries.getWidth() > 0); - assert (maxBoundaries == null || maxBoundaries.getHeight() > 0); - this.maxBoundaries = maxBoundaries; - } - - public MeteringRectangle getAEMeteringRectangle() { - return aeMeteringRectangle; - } - - public MeteringRectangle getAFMeteringRectangle() { - return afMeteringRectangle; - } - - public Size getMaxBoundaries() { - return this.maxBoundaries; - } - - public void resetAutoExposureMeteringRectangle() { - this.aeMeteringRectangle = null; - } - - public void setAutoExposureMeteringRectangleFromPoint(double x, double y) { - this.aeMeteringRectangle = getMeteringRectangleForPoint(maxBoundaries, x, y); - } - - public void resetAutoFocusMeteringRectangle() { - this.afMeteringRectangle = null; - } - - public void setAutoFocusMeteringRectangleFromPoint(double x, double y) { - this.afMeteringRectangle = getMeteringRectangleForPoint(maxBoundaries, x, y); - } - - public MeteringRectangle getMeteringRectangleForPoint(Size maxBoundaries, double x, double y) { - assert (x >= 0 && x <= 1); - assert (y >= 0 && y <= 1); - if (maxBoundaries == null) - throw new IllegalStateException( - "Functionality for managing metering rectangles is unavailable as this CameraRegions instance was initialized with null boundaries."); - - // Interpolate the target coordinate - int targetX = (int) Math.round(x * ((double) (maxBoundaries.getWidth() - 1))); - int targetY = (int) Math.round(y * ((double) (maxBoundaries.getHeight() - 1))); - // Determine the dimensions of the metering triangle (10th of the viewport) - int targetWidth = (int) Math.round(((double) maxBoundaries.getWidth()) / 10d); - int targetHeight = (int) Math.round(((double) maxBoundaries.getHeight()) / 10d); - // Adjust target coordinate to represent top-left corner of metering rectangle - targetX -= targetWidth / 2; - targetY -= targetHeight / 2; - // Adjust target coordinate as to not fall out of bounds - if (targetX < 0) targetX = 0; - if (targetY < 0) targetY = 0; - int maxTargetX = maxBoundaries.getWidth() - 1 - targetWidth; - int maxTargetY = maxBoundaries.getHeight() - 1 - targetHeight; - if (targetX > maxTargetX) targetX = maxTargetX; - if (targetY > maxTargetY) targetY = maxTargetY; - - // Build the metering rectangle - return new MeteringRectangle(targetX, targetY, targetWidth, targetHeight, 1); - } -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index b4d4689f2b4e..003d80a6c241 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -6,20 +6,12 @@ import android.app.Activity; import android.content.Context; -import android.graphics.ImageFormat; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; -import android.hardware.camera2.params.StreamConfigurationMap; -import android.media.CamcorderProfile; -import android.util.Size; import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugins.camera.types.ResolutionPreset; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -29,23 +21,24 @@ public final class CameraUtils { private CameraUtils() {} - static PlatformChannel.DeviceOrientation getDeviceOrientationFromDegrees(int degrees) { - // Round to the nearest 90 degrees. - degrees = (int) (Math.round(degrees / 90.0) * 90) % 360; - // Determine the corresponding device orientation. - switch (degrees) { - case 90: - return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; - case 180: - return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; - case 270: - return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; - case 0: - default: - return PlatformChannel.DeviceOrientation.PORTRAIT_UP; - } + /** + * Gets the {@link CameraManager} singleton. + * + * @param context The context to get the {@link CameraManager} singleton from. + * @return The {@link CameraManager} singleton. + */ + static CameraManager getCameraManager(Context context) { + return (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); } + /** + * Serializes the {@link PlatformChannel.DeviceOrientation} to a string value. + * + * @param orientation The orientation to serialize. + * @return The serialized orientation. + * @throws UnsupportedOperationException when the provided orientation not have a corresponding + * string value. + */ static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orientation) { if (orientation == null) throw new UnsupportedOperationException("Could not serialize null device orientation."); @@ -64,6 +57,15 @@ static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orien } } + /** + * Deserializes a string value to its corresponding {@link PlatformChannel.DeviceOrientation} + * value. + * + * @param orientation The string value to deserialize. + * @return The deserialized orientation. + * @throws UnsupportedOperationException when the provided string value does not have a + * corresponding {@link PlatformChannel.DeviceOrientation}. + */ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String orientation) { if (orientation == null) throw new UnsupportedOperationException("Could not deserialize null device orientation."); @@ -82,23 +84,13 @@ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String ori } } - static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { - if (preset.ordinal() > ResolutionPreset.high.ordinal()) { - preset = ResolutionPreset.high; - } - - CamcorderProfile profile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); - return new Size(profile.videoFrameWidth, profile.videoFrameHeight); - } - - static Size computeBestCaptureSize(StreamConfigurationMap streamConfigurationMap) { - // For still image captures, we use the largest available size. - return Collections.max( - Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), - new CompareSizesByArea()); - } - + /** + * Gets all the available cameras for the device. + * + * @param activity The current Android activity. + * @return A map of all the available cameras, with their name as their key. + * @throws CameraAccessException when the camera could not be accessed. + */ public static List> getAvailableCameras(Activity activity) throws CameraAccessException { CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); @@ -127,52 +119,4 @@ public static List> getAvailableCameras(Activity activity) } return cameras; } - - static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( - String cameraName, ResolutionPreset preset) { - int cameraId = Integer.parseInt(cameraName); - switch (preset) { - // All of these cases deliberately fall through to get the best available profile. - case max: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); - } - case ultraHigh: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P); - } - case veryHigh: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P); - } - case high: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P); - } - case medium: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P); - } - case low: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA); - } - default: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW); - } else { - throw new IllegalArgumentException( - "No capture session available for current capture session."); - } - } - } - - private static class CompareSizesByArea implements Comparator { - @Override - public int compare(Size lhs, Size rhs) { - // We cast here to ensure the multiplications won't overflow. - return Long.signum( - (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); - } - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index 93b963e65821..dc62fce524d3 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -11,8 +11,8 @@ import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; import java.util.HashMap; import java.util.Map; @@ -178,4 +178,28 @@ public void run() { } }); } + + /** + * Send a success payload to a {@link MethodChannel.Result} on the main thread. + * + * @param payload The payload to send. + */ + public void finish(MethodChannel.Result result, Object payload) { + handler.post(() -> result.success(payload)); + } + + /** + * Send an error payload to a {@link MethodChannel.Result} on the main thread. + * + * @param errorCode error code. + * @param errorMessage error message. + * @param errorDetails error details. + */ + public void error( + MethodChannel.Result result, + String errorCode, + @Nullable String errorMessage, + @Nullable Object errorDetails) { + handler.post(() -> result.error(errorCode, errorMessage, errorDetails)); + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java deleted file mode 100644 index 634596dde8bb..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.camera; - -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Configuration; -import android.hardware.SensorManager; -import android.provider.Settings; -import android.view.Display; -import android.view.OrientationEventListener; -import android.view.Surface; -import android.view.WindowManager; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; - -class DeviceOrientationManager { - - private static final IntentFilter orientationIntentFilter = - new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); - - private final Activity activity; - private final DartMessenger messenger; - private final boolean isFrontFacing; - private final int sensorOrientation; - private PlatformChannel.DeviceOrientation lastOrientation; - private OrientationEventListener orientationEventListener; - private BroadcastReceiver broadcastReceiver; - - public DeviceOrientationManager( - Activity activity, DartMessenger messenger, boolean isFrontFacing, int sensorOrientation) { - this.activity = activity; - this.messenger = messenger; - this.isFrontFacing = isFrontFacing; - this.sensorOrientation = sensorOrientation; - } - - public void start() { - startSensorListener(); - startUIListener(); - } - - public void stop() { - stopSensorListener(); - stopUIListener(); - } - - public int getMediaOrientation() { - return this.getMediaOrientation(this.lastOrientation); - } - - public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { - int angle = 0; - - // Fallback to device orientation when the orientation value is null - if (orientation == null) { - orientation = getUIOrientation(); - } - - switch (orientation) { - case PORTRAIT_UP: - angle = 0; - break; - case PORTRAIT_DOWN: - angle = 180; - break; - case LANDSCAPE_LEFT: - angle = 90; - break; - case LANDSCAPE_RIGHT: - angle = 270; - break; - } - if (isFrontFacing) angle *= -1; - return (angle + sensorOrientation + 360) % 360; - } - - private void startSensorListener() { - if (orientationEventListener != null) return; - orientationEventListener = - new OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { - @Override - public void onOrientationChanged(int angle) { - if (!isSystemAutoRotationLocked()) { - PlatformChannel.DeviceOrientation newOrientation = calculateSensorOrientation(angle); - if (!newOrientation.equals(lastOrientation)) { - lastOrientation = newOrientation; - messenger.sendDeviceOrientationChangeEvent(newOrientation); - } - } - } - }; - if (orientationEventListener.canDetectOrientation()) { - orientationEventListener.enable(); - } - } - - private void startUIListener() { - if (broadcastReceiver != null) return; - broadcastReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (isSystemAutoRotationLocked()) { - PlatformChannel.DeviceOrientation orientation = getUIOrientation(); - if (!orientation.equals(lastOrientation)) { - lastOrientation = orientation; - messenger.sendDeviceOrientationChangeEvent(orientation); - } - } - } - }; - activity.registerReceiver(broadcastReceiver, orientationIntentFilter); - broadcastReceiver.onReceive(activity, null); - } - - private void stopSensorListener() { - if (orientationEventListener == null) return; - orientationEventListener.disable(); - orientationEventListener = null; - } - - private void stopUIListener() { - if (broadcastReceiver == null) return; - activity.unregisterReceiver(broadcastReceiver); - broadcastReceiver = null; - } - - private boolean isSystemAutoRotationLocked() { - return android.provider.Settings.System.getInt( - activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) - != 1; - } - - private PlatformChannel.DeviceOrientation getUIOrientation() { - final int rotation = getDisplay().getRotation(); - final int orientation = activity.getResources().getConfiguration().orientation; - - switch (orientation) { - case Configuration.ORIENTATION_PORTRAIT: - if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { - return PlatformChannel.DeviceOrientation.PORTRAIT_UP; - } else { - return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; - } - case Configuration.ORIENTATION_LANDSCAPE: - if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { - return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; - } else { - return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; - } - default: - return PlatformChannel.DeviceOrientation.PORTRAIT_UP; - } - } - - private PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { - final int tolerance = 45; - angle += tolerance; - - // Orientation is 0 in the default orientation mode. This is portait-mode for phones - // and landscape for tablets. We have to compensate for this by calculating the default - // orientation, and apply an offset accordingly. - int defaultDeviceOrientation = getDeviceDefaultOrientation(); - if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) { - angle += 90; - } - // Determine the orientation - angle = angle % 360; - return new PlatformChannel.DeviceOrientation[] { - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - } - [angle / 90]; - } - - private int getDeviceDefaultOrientation() { - Configuration config = activity.getResources().getConfiguration(); - int rotation = getDisplay().getRotation(); - if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) - && config.orientation == Configuration.ORIENTATION_LANDSCAPE) - || ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) - && config.orientation == Configuration.ORIENTATION_PORTRAIT)) { - return Configuration.ORIENTATION_LANDSCAPE; - } else { - return Configuration.ORIENTATION_PORTRAIT; - } - } - - @SuppressWarnings("deprecation") - private Display getDisplay() { - return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); - } -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 50bca6349217..893785f1a58f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -10,6 +10,8 @@ import android.os.Looper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; @@ -17,14 +19,17 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FlashMode; -import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.features.CameraFeatureFactoryImpl; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.flash.FlashMode; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; import io.flutter.view.TextureRegistry; import java.util.HashMap; import java.util.Map; -final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { +final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler, LifecycleObserver { private final Activity activity; private final BinaryMessenger messenger; private final CameraPermissions cameraPermissions; @@ -32,6 +37,7 @@ final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { private final TextureRegistry textureRegistry; private final MethodChannel methodChannel; private final EventChannel imageStreamChannel; + private final Lifecycle lifecycle; private @Nullable Camera camera; MethodCallHandlerImpl( @@ -39,12 +45,14 @@ final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { BinaryMessenger messenger, CameraPermissions cameraPermissions, PermissionsRegistry permissionsAdder, - TextureRegistry textureRegistry) { + TextureRegistry textureRegistry, + @Nullable Lifecycle lifecycle) { this.activity = activity; this.messenger = messenger; this.cameraPermissions = cameraPermissions; this.permissionsRegistry = permissionsAdder; this.textureRegistry = textureRegistry; + this.lifecycle = lifecycle; methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera"); imageStreamChannel = new EventChannel(messenger, "plugins.flutter.io/camera/imageStream"); @@ -172,7 +180,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) y = call.argument("y"); } try { - camera.setExposurePoint(result, x, y); + camera.setExposurePoint(result, new Point(x, y)); } catch (Exception e) { handleException(e, result); } @@ -239,7 +247,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) y = call.argument("y"); } try { - camera.setFocusPoint(result, x, y); + camera.setFocusPoint(result, new Point(x, y)); } catch (Exception e) { handleException(e, result); } @@ -351,22 +359,36 @@ void stopListening() { private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { String cameraName = call.argument("cameraName"); - String resolutionPreset = call.argument("resolutionPreset"); + String preset = call.argument("resolutionPreset"); boolean enableAudio = call.argument("enableAudio"); + TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = textureRegistry.createSurfaceTexture(); DartMessenger dartMessenger = new DartMessenger( messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper())); + CameraProperties cameraProperties = + new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity)); + ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset); + + if (camera != null && lifecycle != null) { + lifecycle.removeObserver(camera); + } + camera = new Camera( activity, flutterSurfaceTexture, + new CameraFeatureFactoryImpl(), dartMessenger, - cameraName, + cameraProperties, resolutionPreset, enableAudio); + if (lifecycle != null) { + lifecycle.addObserver(camera); + } + Map reply = new HashMap<>(); reply.put("cameraId", flutterSurfaceTexture.id()); result.success(reply); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java deleted file mode 100644 index 4c11e2d40e62..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.camera; - -import android.os.Handler; -import android.os.Looper; -import androidx.annotation.Nullable; -import io.flutter.plugin.common.MethodChannel; - -class PictureCaptureRequest { - - enum State { - idle, - focusing, - preCapture, - waitingPreCaptureReady, - capturing, - finished, - error, - } - - private final Runnable timeoutCallback = - new Runnable() { - @Override - public void run() { - error("captureTimeout", "Picture capture request timed out", state.toString()); - } - }; - - private final MethodChannel.Result result; - private final TimeoutHandler timeoutHandler; - private State state; - - public PictureCaptureRequest(MethodChannel.Result result) { - this(result, new TimeoutHandler()); - } - - public PictureCaptureRequest(MethodChannel.Result result, TimeoutHandler timeoutHandler) { - this.result = result; - this.state = State.idle; - this.timeoutHandler = timeoutHandler; - } - - public void setState(State state) { - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - this.state = state; - if (state != State.idle && state != State.finished && state != State.error) { - this.timeoutHandler.resetTimeout(timeoutCallback); - } else { - this.timeoutHandler.clearTimeout(timeoutCallback); - } - } - - public State getState() { - return state; - } - - public boolean isFinished() { - return state == State.finished || state == State.error; - } - - public void finish(String absolutePath) { - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - this.timeoutHandler.clearTimeout(timeoutCallback); - result.success(absolutePath); - state = State.finished; - } - - public void error( - String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - this.timeoutHandler.clearTimeout(timeoutCallback); - result.error(errorCode, errorMessage, errorDetails); - state = State.error; - } - - static class TimeoutHandler { - private static final int REQUEST_TIMEOUT = 5000; - private final Handler handler; - - TimeoutHandler() { - this.handler = new Handler(Looper.getMainLooper()); - } - - public void resetTimeout(Runnable runnable) { - clearTimeout(runnable); - handler.postDelayed(runnable, REQUEST_TIMEOUT); - } - - public void clearTimeout(Runnable runnable) { - handler.removeCallbacks(runnable); - } - } -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java index 8d10c445788c..b91f9a1c03f7 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java @@ -84,9 +84,13 @@ ResolutionFeature createResolutionFeature( * * @param cameraProperties instance of the CameraProperties class containing information about the * cameras features. + * @param sensorOrientationFeature instance of the SensorOrientationFeature class containing + * information about the sensor and device orientation. * @return newly created instance of the FocusPointFeature class. */ - FocusPointFeature createFocusPointFeature(@NonNull CameraProperties cameraProperties); + FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature); /** * Creates a new instance of the FPS range feature. @@ -126,9 +130,13 @@ SensorOrientationFeature createSensorOrientationFeature( * * @param cameraProperties instance of the CameraProperties class containing information about the * cameras features. + * @param sensorOrientationFeature instance of the SensorOrientationFeature class containing + * information about the sensor and device orientation. * @return newly created instance of the ExposurePointFeature class. */ - ExposurePointFeature createExposurePointFeature(@NonNull CameraProperties cameraProperties); + ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature); /** * Creates a new instance of the noise reduction feature. diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java index b12ad3626226..95a8c06caa0a 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java @@ -59,8 +59,10 @@ public ResolutionFeature createResolutionFeature( } @Override - public FocusPointFeature createFocusPointFeature(@NonNull CameraProperties cameraProperties) { - return new FocusPointFeature(cameraProperties); + public FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { + return new FocusPointFeature(cameraProperties, sensorOrientationFeature); } @Override @@ -83,8 +85,9 @@ public ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraP @Override public ExposurePointFeature createExposurePointFeature( - @NonNull CameraProperties cameraProperties) { - return new ExposurePointFeature(cameraProperties); + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { + return new ExposurePointFeature(cameraProperties, sensorOrientationFeature); } @Override diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java index 0ee8969071bc..659fd15963e9 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java @@ -4,6 +4,9 @@ package io.flutter.plugins.camera.features; +import android.app.Activity; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.DartMessenger; import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; @@ -13,6 +16,7 @@ import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; import java.util.Collection; @@ -37,6 +41,39 @@ public class CameraFeatures { private static final String SENSOR_ORIENTATION = "SENSOR_ORIENTATION"; private static final String ZOOM_LEVEL = "ZOOM_LEVEL"; + public static CameraFeatures init( + CameraFeatureFactory cameraFeatureFactory, + CameraProperties cameraProperties, + Activity activity, + DartMessenger dartMessenger, + ResolutionPreset resolutionPreset) { + CameraFeatures cameraFeatures = new CameraFeatures(); + cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); + cameraFeatures.setExposureLock( + cameraFeatureFactory.createExposureLockFeature(cameraProperties)); + cameraFeatures.setExposureOffset( + cameraFeatureFactory.createExposureOffsetFeature(cameraProperties)); + SensorOrientationFeature sensorOrientationFeature = + cameraFeatureFactory.createSensorOrientationFeature( + cameraProperties, activity, dartMessenger); + cameraFeatures.setSensorOrientation(sensorOrientationFeature); + cameraFeatures.setExposurePoint( + cameraFeatureFactory.createExposurePointFeature( + cameraProperties, sensorOrientationFeature)); + cameraFeatures.setFlash(cameraFeatureFactory.createFlashFeature(cameraProperties)); + cameraFeatures.setFocusPoint( + cameraFeatureFactory.createFocusPointFeature(cameraProperties, sensorOrientationFeature)); + cameraFeatures.setFpsRange(cameraFeatureFactory.createFpsRangeFeature(cameraProperties)); + cameraFeatures.setNoiseReduction( + cameraFeatureFactory.createNoiseReductionFeature(cameraProperties)); + cameraFeatures.setResolution( + cameraFeatureFactory.createResolutionFeature( + cameraProperties, resolutionPreset, cameraProperties.getCameraName())); + cameraFeatures.setZoomLevel(cameraFeatureFactory.createZoomLevelFeature(cameraProperties)); + return cameraFeatures; + } + private Map featureMap = new HashMap<>(); /** diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java index 8c2ee6167846..336e756e9ed8 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java @@ -8,10 +8,12 @@ import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.CameraFeature; import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; /** Exposure point controls where in the frame exposure metering will come from. */ public class ExposurePointFeature extends CameraFeature { @@ -19,14 +21,17 @@ public class ExposurePointFeature extends CameraFeature { private Size cameraBoundaries; private Point exposurePoint; private MeteringRectangle exposureRectangle; + private final SensorOrientationFeature sensorOrientationFeature; /** * Creates a new instance of the {@link ExposurePointFeature}. * * @param cameraProperties Collection of the characteristics for the current camera device. */ - public ExposurePointFeature(CameraProperties cameraProperties) { + public ExposurePointFeature( + CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) { super(cameraProperties); + this.sensorOrientationFeature = sensorOrientationFeature; } /** @@ -80,9 +85,15 @@ private void buildExposureRectangle() { if (this.exposurePoint == null) { this.exposureRectangle = null; } else { + PlatformChannel.DeviceOrientation orientation = + this.sensorOrientationFeature.getLockedCaptureOrientation(); + if (orientation == null) { + orientation = + this.sensorOrientationFeature.getDeviceOrientationManager().getLastUIOrientation(); + } this.exposureRectangle = CameraRegionUtils.convertPointToMeteringRectangle( - this.cameraBoundaries, this.exposurePoint.x, this.exposurePoint.y); + this.cameraBoundaries, this.exposurePoint.x, this.exposurePoint.y, orientation); } } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java index 92fcfa9f1132..a3a0172d3c37 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java @@ -8,10 +8,12 @@ import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.CameraFeature; import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; /** Focus point controls where in the frame focus will come from. */ public class FocusPointFeature extends CameraFeature { @@ -19,14 +21,17 @@ public class FocusPointFeature extends CameraFeature { private Size cameraBoundaries; private Point focusPoint; private MeteringRectangle focusRectangle; + private final SensorOrientationFeature sensorOrientationFeature; /** * Creates a new instance of the {@link FocusPointFeature}. * * @param cameraProperties Collection of the characteristics for the current camera device. */ - public FocusPointFeature(CameraProperties cameraProperties) { + public FocusPointFeature( + CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) { super(cameraProperties); + this.sensorOrientationFeature = sensorOrientationFeature; } /** @@ -80,9 +85,15 @@ private void buildFocusRectangle() { if (this.focusPoint == null) { this.focusRectangle = null; } else { + PlatformChannel.DeviceOrientation orientation = + this.sensorOrientationFeature.getLockedCaptureOrientation(); + if (orientation == null) { + orientation = + this.sensorOrientationFeature.getDeviceOrientationManager().getLastUIOrientation(); + } this.focusRectangle = CameraRegionUtils.convertPointToMeteringRectangle( - this.cameraBoundaries, this.focusPoint.x, this.focusPoint.y); + this.cameraBoundaries, this.focusPoint.x, this.focusPoint.y, orientation); } } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java index 847a817641ab..408575b375e6 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java @@ -20,9 +20,15 @@ public class NoiseReductionFeature extends CameraFeature { private NoiseReductionMode currentSetting = NoiseReductionMode.fast; - private static final HashMap NOISE_REDUCTION_MODES = new HashMap<>(); + private final HashMap NOISE_REDUCTION_MODES = new HashMap<>(); - static { + /** + * Creates a new instance of the {@link NoiseReductionFeature}. + * + * @param cameraProperties Collection of the characteristics for the current camera device. + */ + public NoiseReductionFeature(CameraProperties cameraProperties) { + super(cameraProperties); NOISE_REDUCTION_MODES.put(NoiseReductionMode.off, CaptureRequest.NOISE_REDUCTION_MODE_OFF); NOISE_REDUCTION_MODES.put(NoiseReductionMode.fast, CaptureRequest.NOISE_REDUCTION_MODE_FAST); NOISE_REDUCTION_MODES.put( @@ -35,15 +41,6 @@ public class NoiseReductionFeature extends CameraFeature { } } - /** - * Creates a new instance of the {@link NoiseReductionFeature}. - * - * @param cameraProperties Collection of the characteristics for the current camera device. - */ - public NoiseReductionFeature(CameraProperties cameraProperties) { - super(cameraProperties); - } - @Override public String getDebugName() { return "NoiseReductionFeature"; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java index 2a04caad743a..dd1e489e6225 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java @@ -10,10 +10,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; -import android.hardware.SensorManager; -import android.provider.Settings; import android.view.Display; -import android.view.OrientationEventListener; import android.view.Surface; import android.view.WindowManager; import androidx.annotation.NonNull; @@ -35,7 +32,6 @@ public class DeviceOrientationManager { private final boolean isFrontFacing; private final int sensorOrientation; private PlatformChannel.DeviceOrientation lastOrientation; - private OrientationEventListener orientationEventListener; private BroadcastReceiver broadcastReceiver; /** Factory method to create a device orientation manager. */ @@ -63,7 +59,7 @@ private DeviceOrientationManager( * *

    When orientation information is updated the new orientation is send to the client using the * {@link DartMessenger}. This latest value can also be retrieved through the {@link - * #getMediaOrientation()} accessor. + * #getVideoOrientation()} accessor. * *

    If the device's ACCELEROMETER_ROTATION setting is enabled the {@link * DeviceOrientationManager} will report orientation updates based on the sensor information. If @@ -71,55 +67,106 @@ private DeviceOrientationManager( * the deliver orientation updates based on the UI orientation. */ public void start() { - startSensorListener(); - startUIListener(); + if (broadcastReceiver != null) { + return; + } + broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleUIOrientationChange(); + } + }; + activity.registerReceiver(broadcastReceiver, orientationIntentFilter); + broadcastReceiver.onReceive(activity, null); } /** Stops listening for orientation updates. */ public void stop() { - stopSensorListener(); - stopUIListener(); + if (broadcastReceiver == null) { + return; + } + activity.unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; } /** - * Returns the last captured orientation in degrees based on sensor or UI information. + * Returns the device's photo orientation in degrees based on the sensor orientation and the last + * known UI orientation. * - *

    The orientation is returned in degrees and could be one of the following values: + *

    Returns one of 0, 90, 180 or 270. * - *

      - *
    • 0: Indicates the device is currently in portrait. - *
    • 90: Indicates the device is currently in landscape left. - *
    • 180: Indicates the device is currently in portrait down. - *
    • 270: Indicates the device is currently in landscape right. - *
    + * @return The device's photo orientation in degrees. + */ + public int getPhotoOrientation() { + return this.getPhotoOrientation(this.lastOrientation); + } + + /** + * Returns the device's photo orientation in degrees based on the sensor orientation and the + * supplied {@link PlatformChannel.DeviceOrientation} value. * - * @return The last captured orientation in degrees + *

    Returns one of 0, 90, 180 or 270. + * + * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted + * into degrees. + * @return The device's photo orientation in degrees. */ - public int getMediaOrientation() { - return this.getMediaOrientation(this.lastOrientation); + public int getPhotoOrientation(PlatformChannel.DeviceOrientation orientation) { + int angle = 0; + // Fallback to device orientation when the orientation value is null. + if (orientation == null) { + orientation = getUIOrientation(); + } + + switch (orientation) { + case PORTRAIT_UP: + angle = 90; + break; + case PORTRAIT_DOWN: + angle = 270; + break; + case LANDSCAPE_LEFT: + angle = isFrontFacing ? 180 : 0; + break; + case LANDSCAPE_RIGHT: + angle = isFrontFacing ? 0 : 180; + break; + } + + // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X). + // This has to be taken into account so the JPEG is rotated properly. + // For devices with orientation of 90, this simply returns the mapping from ORIENTATIONS. + // For devices with orientation of 270, the JPEG is rotated 180 degrees instead. + return (angle + sensorOrientation + 270) % 360; } /** - * Returns the device's orientation in degrees based on the supplied {@link - * PlatformChannel.DeviceOrientation} value. + * Returns the device's video orientation in degrees based on the sensor orientation and the last + * known UI orientation. + * + *

    Returns one of 0, 90, 180 or 270. * - *

    + * @return The device's video orientation in degrees. + */ + public int getVideoOrientation() { + return this.getVideoOrientation(this.lastOrientation); + } + + /** + * Returns the device's video orientation in degrees based on the sensor orientation and the + * supplied {@link PlatformChannel.DeviceOrientation} value. * - *

      - *
    • PORTRAIT_UP: converts to 0 degrees. - *
    • LANDSCAPE_LEFT: converts to 90 degrees. - *
    • PORTRAIT_DOWN: converts to 180 degrees. - *
    • LANDSCAPE_RIGHT: converts to 270 degrees. - *
    + *

    Returns one of 0, 90, 180 or 270. * * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted * into degrees. - * @return The device's orientation in degrees. + * @return The device's video orientation in degrees. */ - public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { + public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) { int angle = 0; - // Fallback to device orientation when the orientation value is null + // Fallback to device orientation when the orientation value is null. if (orientation == null) { orientation = getUIOrientation(); } @@ -146,51 +193,9 @@ public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { return (angle + sensorOrientation + 360) % 360; } - private void startSensorListener() { - if (orientationEventListener != null) { - return; - } - orientationEventListener = - new OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { - @Override - public void onOrientationChanged(int angle) { - handleSensorOrientationChange(angle); - } - }; - if (orientationEventListener.canDetectOrientation()) { - orientationEventListener.enable(); - } - } - - private void startUIListener() { - if (broadcastReceiver != null) { - return; - } - broadcastReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - handleUIOrientationChange(); - } - }; - activity.registerReceiver(broadcastReceiver, orientationIntentFilter); - broadcastReceiver.onReceive(activity, null); - } - - /** - * Handles orientation changes based on information from the device's sensors. - * - *

    This method is visible for testing purposes only and should never be used outside this - * class. - * - * @param angle of the current orientation. - */ - @VisibleForTesting - void handleSensorOrientationChange(int angle) { - if (!isAccelerometerRotationLocked()) { - PlatformChannel.DeviceOrientation orientation = calculateSensorOrientation(angle); - lastOrientation = handleOrientationChange(orientation, lastOrientation, messenger); - } + /** @return the last received UI orientation. */ + public PlatformChannel.DeviceOrientation getLastUIOrientation() { + return this.lastOrientation; } /** @@ -201,10 +206,9 @@ void handleSensorOrientationChange(int angle) { */ @VisibleForTesting void handleUIOrientationChange() { - if (isAccelerometerRotationLocked()) { - PlatformChannel.DeviceOrientation orientation = getUIOrientation(); - lastOrientation = handleOrientationChange(orientation, lastOrientation, messenger); - } + PlatformChannel.DeviceOrientation orientation = getUIOrientation(); + handleOrientationChange(orientation, lastOrientation, messenger); + lastOrientation = orientation; } /** @@ -215,37 +219,13 @@ void handleUIOrientationChange() { * class. */ @VisibleForTesting - static DeviceOrientation handleOrientationChange( + static void handleOrientationChange( DeviceOrientation newOrientation, DeviceOrientation previousOrientation, DartMessenger messenger) { if (!newOrientation.equals(previousOrientation)) { messenger.sendDeviceOrientationChangeEvent(newOrientation); } - - return newOrientation; - } - - private void stopSensorListener() { - if (orientationEventListener == null) { - return; - } - orientationEventListener.disable(); - orientationEventListener = null; - } - - private void stopUIListener() { - if (broadcastReceiver == null) { - return; - } - activity.unregisterReceiver(broadcastReceiver); - broadcastReceiver = null; - } - - private boolean isAccelerometerRotationLocked() { - return android.provider.Settings.System.getInt( - activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) - != 1; } /** diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java index 2c0381744191..40db12ee0fc3 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java @@ -41,7 +41,7 @@ public void before() { } @Test - public void ctor_Should_return_valid_instance() throws CameraAccessException { + public void ctor_shouldReturnValidInstance() throws CameraAccessException { verify(mockCameraManager, times(1)).getCameraCharacteristics(CAMERA_NAME); assertNotNull(cameraProperties); } @@ -76,8 +76,7 @@ public void getControlAutoExposureCompensationRangeTest() { } @Test - public void - getControlAutoExposureCompensationStep_Should_return_double_When_rational_is_not_null() { + public void getControlAutoExposureCompensationStep_shouldReturnDoubleWhenRationalIsNotNull() { double expectedStep = 3.1415926535; Rational mockRational = mock(Rational.class); @@ -92,7 +91,7 @@ public void getControlAutoExposureCompensationRangeTest() { } @Test - public void getControlAutoExposureCompensationStep_Should_return_zero_When_rational_is_null() { + public void getControlAutoExposureCompensationStep_shouldReturnZeroWhenRationalIsNull() { double expectedStep = 0.0; when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP)) diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_convertPointToMeteringRectangleTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_convertPointToMeteringRectangleTest.java new file mode 100644 index 000000000000..2c6d9d9177e9 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_convertPointToMeteringRectangleTest.java @@ -0,0 +1,197 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Size; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +public class CameraRegionUtils_convertPointToMeteringRectangleTest { + private MockedStatic mockedMeteringRectangleFactory; + private Size mockCameraBoundaries; + + @Before + public void setUp() { + this.mockCameraBoundaries = mock(Size.class); + when(this.mockCameraBoundaries.getWidth()).thenReturn(100); + when(this.mockCameraBoundaries.getHeight()).thenReturn(100); + mockedMeteringRectangleFactory = mockStatic(CameraRegionUtils.MeteringRectangleFactory.class); + + mockedMeteringRectangleFactory + .when( + () -> + CameraRegionUtils.MeteringRectangleFactory.create( + anyInt(), anyInt(), anyInt(), anyInt(), anyInt())) + .thenAnswer( + new Answer() { + @Override + public MeteringRectangle answer(InvocationOnMock createInvocation) throws Throwable { + MeteringRectangle mockMeteringRectangle = mock(MeteringRectangle.class); + when(mockMeteringRectangle.getX()).thenReturn(createInvocation.getArgument(0)); + when(mockMeteringRectangle.getY()).thenReturn(createInvocation.getArgument(1)); + when(mockMeteringRectangle.getWidth()).thenReturn(createInvocation.getArgument(2)); + when(mockMeteringRectangle.getHeight()).thenReturn(createInvocation.getArgument(3)); + when(mockMeteringRectangle.getMeteringWeight()) + .thenReturn(createInvocation.getArgument(4)); + when(mockMeteringRectangle.equals(any())) + .thenAnswer( + new Answer() { + @Override + public Boolean answer(InvocationOnMock equalsInvocation) + throws Throwable { + MeteringRectangle otherMockMeteringRectangle = + equalsInvocation.getArgument(0); + return mockMeteringRectangle.getX() == otherMockMeteringRectangle.getX() + && mockMeteringRectangle.getY() == otherMockMeteringRectangle.getY() + && mockMeteringRectangle.getWidth() + == otherMockMeteringRectangle.getWidth() + && mockMeteringRectangle.getHeight() + == otherMockMeteringRectangle.getHeight() + && mockMeteringRectangle.getMeteringWeight() + == otherMockMeteringRectangle.getMeteringWeight(); + } + }); + return mockMeteringRectangle; + } + }); + } + + @After + public void tearDown() { + mockedMeteringRectangleFactory.close(); + } + + @Test + public void convertPointToMeteringRectangle_shouldReturnValidMeteringRectangleForCenterCoord() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0.5, 0.5, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(45, 45, 10, 10, 1).equals(r)); + } + + @Test + public void convertPointToMeteringRectangle_shouldReturnValidMeteringRectangleForTopLeftCoord() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, 0, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 0, 10, 10, 1).equals(r)); + } + + @Test + public void convertPointToMeteringRectangle_ShouldReturnValidMeteringRectangleForTopRightCoord() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 0, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 0, 10, 10, 1).equals(r)); + } + + @Test + public void + convertPointToMeteringRectangle_shouldReturnValidMeteringRectangleForBottomLeftCoord() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, 1, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 89, 10, 10, 1).equals(r)); + } + + @Test + public void + convertPointToMeteringRectangle_shouldReturnValidMeteringRectangleForBottomRightCoord() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 89, 10, 10, 1).equals(r)); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowForXUpperBound() { + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1.5, 0, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowForXLowerBound() { + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, -0.5, 0, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowForYUpperBound() { + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, 1.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowForYLowerBound() { + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, -0.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test() + public void + convertPointToMeteringRectangle_shouldRotateMeteringRectangleAccordingToUiOrientationForPortraitUp() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 0, 10, 10, 1).equals(r)); + } + + @Test() + public void + convertPointToMeteringRectangle_shouldRotateMeteringRectangleAccordingToUiOrientationForPortraitDown() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.PORTRAIT_DOWN); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 89, 10, 10, 1).equals(r)); + } + + @Test() + public void + convertPointToMeteringRectangle_shouldRotateMeteringRectangleAccordingToUiOrientationForLandscapeLeft() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 89, 10, 10, 1).equals(r)); + } + + @Test() + public void + convertPointToMeteringRectangle_shouldRotateMeteringRectangleAccordingToUiOrientationForLandscapeRight() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 0, 10, 10, 1).equals(r)); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowFor0WidthBoundary() { + Size mockCameraBoundaries = mock(Size.class); + when(mockCameraBoundaries.getWidth()).thenReturn(0); + when(mockCameraBoundaries.getHeight()).thenReturn(50); + CameraRegionUtils.convertPointToMeteringRectangle( + mockCameraBoundaries, 0, -0.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowFor0HeightBoundary() { + Size mockCameraBoundaries = mock(Size.class); + when(mockCameraBoundaries.getWidth()).thenReturn(50); + when(mockCameraBoundaries.getHeight()).thenReturn(0); + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, -0.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_getCameraBoundariesTest.java similarity index 61% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java rename to packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_getCameraBoundariesTest.java index 2d65c4e0fc05..4c0164981b74 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_getCameraBoundariesTest.java @@ -4,8 +4,6 @@ package io.flutter.plugins.camera; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; @@ -15,17 +13,15 @@ import android.graphics.Rect; import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.params.MeteringRectangle; import android.os.Build; import android.util.Size; import io.flutter.plugins.camera.utils.TestUtils; import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; -import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -public class CameraRegionUtilsTest { +public class CameraRegionUtils_getCameraBoundariesTest { Size mockCameraBoundaries; @@ -37,8 +33,7 @@ public void setUp() { } @Test - public void - getCameraBoundaries_should_return_sensor_info_pixel_array_size_when_running_pre_android_p() { + public void getCameraBoundaries_shouldReturnSensorInfoPixelArraySizeWhenRunningPreAndroidP() { updateSdkVersion(Build.VERSION_CODES.O_MR1); try { @@ -58,7 +53,7 @@ public void setUp() { @Test public void - getCameraBoundaries_should_return_sensor_info_pixel_array_size_when_distortion_correction_is_null() { + getCameraBoundaries_shouldReturnSensorInfoPixelArraySizeWhenDistortionCorrectionIsNull() { updateSdkVersion(Build.VERSION_CODES.P); try { @@ -80,7 +75,7 @@ public void setUp() { @Test public void - getCameraBoundaries_should_return_sensor_info_pixel_array_size_when_distortion_correction_is_off() { + getCameraBoundaries_shouldReturnSensorInfoPixelArraySizeWhenDistortionCorrectionIsOff() { updateSdkVersion(Build.VERSION_CODES.P); try { @@ -103,7 +98,7 @@ public void setUp() { @Test public void - getCameraBoundaries_should_return_info_pre_correction_active_array_size_when_distortion_correction_mode_is_set_to_null() { + getCameraBoundaries_shouldReturnInfoPreCorrectionActiveArraySizeWhenDistortionCorrectionModeIsSetToNull() { updateSdkVersion(Build.VERSION_CODES.P); try { @@ -150,7 +145,7 @@ public void setUp() { @Test public void - getCameraBoundaries_should_return_info_pre_correction_active_array_size_when_distortion_correction_mode_is_set_to_off() { + getCameraBoundaries_shouldReturnInfoPreCorrectionActiveArraySizeWhenDistortionCorrectionModeIsSetToOff() { updateSdkVersion(Build.VERSION_CODES.P); try { @@ -199,7 +194,7 @@ public void setUp() { @Test public void - getCameraBoundaries_should_return_sensor_info_active_array_size_when_distortion_correction_mode_is_set() { + getCameraBoundaries_shouldReturnSensorInfoActiveArraySizeWhenDistortionCorrectionModeIsSet() { updateSdkVersion(Build.VERSION_CODES.P); try { @@ -246,107 +241,6 @@ public void setUp() { } } - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_x_upper_bound() { - CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 1.5, 0); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_x_lower_bound() { - CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, -0.5, 0); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_y_upper_bound() { - CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0, 1.5); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_y_lower_bound() { - CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0, -0.5); - } - - @Test - public void getMeteringRectangleForPoint_should_return_valid_MeteringRectangle() { - try (MockedStatic mockedMeteringRectangleFactory = - mockStatic(CameraRegionUtils.MeteringRectangleFactory.class)) { - - mockedMeteringRectangleFactory - .when( - () -> - CameraRegionUtils.MeteringRectangleFactory.create( - anyInt(), anyInt(), anyInt(), anyInt(), anyInt())) - .thenAnswer( - new Answer() { - @Override - public MeteringRectangle answer(InvocationOnMock createInvocation) - throws Throwable { - MeteringRectangle mockMeteringRectangle = mock(MeteringRectangle.class); - when(mockMeteringRectangle.getX()).thenReturn(createInvocation.getArgument(0)); - when(mockMeteringRectangle.getY()).thenReturn(createInvocation.getArgument(1)); - when(mockMeteringRectangle.getWidth()) - .thenReturn(createInvocation.getArgument(2)); - when(mockMeteringRectangle.getHeight()) - .thenReturn(createInvocation.getArgument(3)); - when(mockMeteringRectangle.getMeteringWeight()) - .thenReturn(createInvocation.getArgument(4)); - when(mockMeteringRectangle.equals(any())) - .thenAnswer( - new Answer() { - @Override - public Boolean answer(InvocationOnMock equalsInvocation) - throws Throwable { - MeteringRectangle otherMockMeteringRectangle = - equalsInvocation.getArgument(0); - return mockMeteringRectangle.getX() - == otherMockMeteringRectangle.getX() - && mockMeteringRectangle.getY() - == otherMockMeteringRectangle.getY() - && mockMeteringRectangle.getWidth() - == otherMockMeteringRectangle.getWidth() - && mockMeteringRectangle.getHeight() - == otherMockMeteringRectangle.getHeight() - && mockMeteringRectangle.getMeteringWeight() - == otherMockMeteringRectangle.getMeteringWeight(); - } - }); - return mockMeteringRectangle; - } - }); - - MeteringRectangle r; - // Center - r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0.5, 0.5); - assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(45, 45, 10, 10, 1).equals(r)); - - // Top left - r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0.0, 0.0); - assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 0, 10, 10, 1).equals(r)); - - // Bottom right - r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 1.0, 1.0); - assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 89, 10, 10, 1).equals(r)); - - // Top left - r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0.0, 1.0); - assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 89, 10, 10, 1).equals(r)); - - // Top right - r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 1.0, 0.0); - assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 0, 10, 10, 1).equals(r)); - } - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_0_width_boundary() { - new io.flutter.plugins.camera.CameraRegions(new Size(0, 50)); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_0_height_boundary() { - new io.flutter.plugins.camera.CameraRegions(new Size(100, 0)); - } - private static void updateSdkVersion(int version) { TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", version); } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java new file mode 100644 index 000000000000..cab2ae8974a4 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -0,0 +1,843 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; +import android.media.CamcorderProfile; +import android.media.MediaRecorder; +import android.os.Build; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.camera.features.CameraFeatureFactory; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; +import io.flutter.plugins.camera.features.flash.FlashMode; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; +import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +import io.flutter.plugins.camera.utils.TestUtils; +import io.flutter.view.TextureRegistry; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class CameraTest { + private CameraProperties mockCameraProperties; + private CameraFeatureFactory mockCameraFeatureFactory; + private DartMessenger mockDartMessenger; + private Camera camera; + private CameraCaptureSession mockCaptureSession; + private CaptureRequest.Builder mockPreviewRequestBuilder; + + @Before + public void before() { + mockCameraProperties = mock(CameraProperties.class); + mockCameraFeatureFactory = new TestCameraFeatureFactory(); + mockDartMessenger = mock(DartMessenger.class); + mockCaptureSession = mock(CameraCaptureSession.class); + mockPreviewRequestBuilder = mock(CaptureRequest.Builder.class); + + final Activity mockActivity = mock(Activity.class); + final TextureRegistry.SurfaceTextureEntry mockFlutterTexture = + mock(TextureRegistry.SurfaceTextureEntry.class); + final String cameraName = "1"; + final ResolutionPreset resolutionPreset = ResolutionPreset.high; + final boolean enableAudio = false; + + when(mockCameraProperties.getCameraName()).thenReturn(cameraName); + + camera = + new Camera( + mockActivity, + mockFlutterTexture, + mockCameraFeatureFactory, + mockDartMessenger, + mockCameraProperties, + resolutionPreset, + enableAudio); + + TestUtils.setPrivateField(camera, "captureSession", mockCaptureSession); + TestUtils.setPrivateField(camera, "previewRequestBuilder", mockPreviewRequestBuilder); + } + + @After + public void after() { + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 0); + } + + @Test + public void shouldCreateCameraPluginAndSetAllFeatures() { + final Activity mockActivity = mock(Activity.class); + final TextureRegistry.SurfaceTextureEntry mockFlutterTexture = + mock(TextureRegistry.SurfaceTextureEntry.class); + final CameraFeatureFactory mockCameraFeatureFactory = mock(CameraFeatureFactory.class); + final String cameraName = "1"; + final ResolutionPreset resolutionPreset = ResolutionPreset.high; + final boolean enableAudio = false; + + when(mockCameraProperties.getCameraName()).thenReturn(cameraName); + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + when(mockCameraFeatureFactory.createSensorOrientationFeature(any(), any(), any())) + .thenReturn(mockSensorOrientationFeature); + + Camera camera = + new Camera( + mockActivity, + mockFlutterTexture, + mockCameraFeatureFactory, + mockDartMessenger, + mockCameraProperties, + resolutionPreset, + enableAudio); + + verify(mockCameraFeatureFactory, times(1)) + .createSensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + verify(mockCameraFeatureFactory, times(1)).createAutoFocusFeature(mockCameraProperties, false); + verify(mockCameraFeatureFactory, times(1)).createExposureLockFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)) + .createExposurePointFeature(eq(mockCameraProperties), eq(mockSensorOrientationFeature)); + verify(mockCameraFeatureFactory, times(1)).createExposureOffsetFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)).createFlashFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)) + .createFocusPointFeature(eq(mockCameraProperties), eq(mockSensorOrientationFeature)); + verify(mockCameraFeatureFactory, times(1)).createFpsRangeFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)).createNoiseReductionFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)) + .createResolutionFeature(mockCameraProperties, resolutionPreset, cameraName); + verify(mockCameraFeatureFactory, times(1)).createZoomLevelFeature(mockCameraProperties); + assertNotNull("should create a camera", camera); + } + + @Test + public void getDeviceOrientationManager() { + SensorOrientationFeature mockSensorOrientationFeature = + mockCameraFeatureFactory.createSensorOrientationFeature(mockCameraProperties, null, null); + DeviceOrientationManager mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + + when(mockSensorOrientationFeature.getDeviceOrientationManager()) + .thenReturn(mockDeviceOrientationManager); + + DeviceOrientationManager actualDeviceOrientationManager = camera.getDeviceOrientationManager(); + + verify(mockSensorOrientationFeature, times(1)).getDeviceOrientationManager(); + assertEquals(mockDeviceOrientationManager, actualDeviceOrientationManager); + } + + @Test + public void getExposureOffsetStepSize() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + double stepSize = 2.3; + + when(mockExposureOffsetFeature.getExposureOffsetStepSize()).thenReturn(stepSize); + + double actualSize = camera.getExposureOffsetStepSize(); + + verify(mockExposureOffsetFeature, times(1)).getExposureOffsetStepSize(); + assertEquals(stepSize, actualSize, 0); + } + + @Test + public void getMaxExposureOffset() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + double expectedMaxOffset = 42.0; + + when(mockExposureOffsetFeature.getMaxExposureOffset()).thenReturn(expectedMaxOffset); + + double actualMaxOffset = camera.getMaxExposureOffset(); + + verify(mockExposureOffsetFeature, times(1)).getMaxExposureOffset(); + assertEquals(expectedMaxOffset, actualMaxOffset, 0); + } + + @Test + public void getMinExposureOffset() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + double expectedMinOffset = 21.5; + + when(mockExposureOffsetFeature.getMinExposureOffset()).thenReturn(21.5); + + double actualMinOffset = camera.getMinExposureOffset(); + + verify(mockExposureOffsetFeature, times(1)).getMinExposureOffset(); + assertEquals(expectedMinOffset, actualMinOffset, 0); + } + + @Test + public void getMaxZoomLevel() { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + float expectedMaxZoomLevel = 4.2f; + + when(mockZoomLevelFeature.getMaximumZoomLevel()).thenReturn(expectedMaxZoomLevel); + + float actualMaxZoomLevel = camera.getMaxZoomLevel(); + + verify(mockZoomLevelFeature, times(1)).getMaximumZoomLevel(); + assertEquals(expectedMaxZoomLevel, actualMaxZoomLevel, 0); + } + + @Test + public void getMinZoomLevel() { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + float expectedMinZoomLevel = 4.2f; + + when(mockZoomLevelFeature.getMinimumZoomLevel()).thenReturn(expectedMinZoomLevel); + + float actualMinZoomLevel = camera.getMinZoomLevel(); + + verify(mockZoomLevelFeature, times(1)).getMinimumZoomLevel(); + assertEquals(expectedMinZoomLevel, actualMinZoomLevel, 0); + } + + @Test + public void getRecordingProfile() { + ResolutionFeature mockResolutionFeature = + mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null); + CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); + + when(mockResolutionFeature.getRecordingProfile()).thenReturn(mockCamcorderProfile); + + CamcorderProfile actualRecordingProfile = camera.getRecordingProfile(); + + verify(mockResolutionFeature, times(1)).getRecordingProfile(); + assertEquals(mockCamcorderProfile, actualRecordingProfile); + } + + @Test + public void setExposureMode_shouldUpdateExposureLockFeature() { + ExposureLockFeature mockExposureLockFeature = + mockCameraFeatureFactory.createExposureLockFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + ExposureMode exposureMode = ExposureMode.locked; + + camera.setExposureMode(mockResult, exposureMode); + + verify(mockExposureLockFeature, times(1)).setValue(exposureMode); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setExposureMode_shouldUpdateBuilder() { + ExposureLockFeature mockExposureLockFeature = + mockCameraFeatureFactory.createExposureLockFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + ExposureMode exposureMode = ExposureMode.locked; + + camera.setExposureMode(mockResult, exposureMode); + + verify(mockExposureLockFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setExposureMode_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + ExposureMode exposureMode = ExposureMode.locked; + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setExposureMode(mockResult, exposureMode); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)) + .error("setExposureModeFailed", "Could not set exposure mode.", null); + } + + @Test + public void setExposurePoint_shouldUpdateExposurePointFeature() { + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + ExposurePointFeature mockExposurePointFeature = + mockCameraFeatureFactory.createExposurePointFeature( + mockCameraProperties, mockSensorOrientationFeature); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + + camera.setExposurePoint(mockResult, point); + + verify(mockExposurePointFeature, times(1)).setValue(point); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setExposurePoint_shouldUpdateBuilder() { + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + ExposurePointFeature mockExposurePointFeature = + mockCameraFeatureFactory.createExposurePointFeature( + mockCameraProperties, mockSensorOrientationFeature); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + + camera.setExposurePoint(mockResult, point); + + verify(mockExposurePointFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setExposurePoint_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setExposurePoint(mockResult, point); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)) + .error("setExposurePointFailed", "Could not set exposure point.", null); + } + + @Test + public void setFlashMode_shouldUpdateFlashFeature() { + FlashFeature mockFlashFeature = + mockCameraFeatureFactory.createFlashFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + FlashMode flashMode = FlashMode.always; + + camera.setFlashMode(mockResult, flashMode); + + verify(mockFlashFeature, times(1)).setValue(flashMode); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setFlashMode_shouldUpdateBuilder() { + FlashFeature mockFlashFeature = + mockCameraFeatureFactory.createFlashFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + FlashMode flashMode = FlashMode.always; + + camera.setFlashMode(mockResult, flashMode); + + verify(mockFlashFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setFlashMode_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + FlashMode flashMode = FlashMode.always; + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setFlashMode(mockResult, flashMode); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)).error("setFlashModeFailed", "Could not set flash mode.", null); + } + + @Test + public void setFocusPoint_shouldUpdateFocusPointFeature() { + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + FocusPointFeature mockFocusPointFeature = + mockCameraFeatureFactory.createFocusPointFeature( + mockCameraProperties, mockSensorOrientationFeature); + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + when(mockAutoFocusFeature.getValue()).thenReturn(FocusMode.auto); + + camera.setFocusPoint(mockResult, point); + + verify(mockFocusPointFeature, times(1)).setValue(point); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setFocusPoint_shouldUpdateBuilder() { + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + FocusPointFeature mockFocusPointFeature = + mockCameraFeatureFactory.createFocusPointFeature( + mockCameraProperties, mockSensorOrientationFeature); + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + when(mockAutoFocusFeature.getValue()).thenReturn(FocusMode.auto); + + camera.setFocusPoint(mockResult, point); + + verify(mockFocusPointFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setFocusPoint_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + when(mockAutoFocusFeature.getValue()).thenReturn(FocusMode.auto); + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setFocusPoint(mockResult, point); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)).error("setFocusPointFailed", "Could not set focus point.", null); + } + + @Test + public void setZoomLevel_shouldUpdateZoomLevelFeature() throws CameraAccessException { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + float zoomLevel = 1.0f; + + when(mockZoomLevelFeature.getValue()).thenReturn(zoomLevel); + when(mockZoomLevelFeature.getMinimumZoomLevel()).thenReturn(0f); + when(mockZoomLevelFeature.getMaximumZoomLevel()).thenReturn(2f); + + camera.setZoomLevel(mockResult, zoomLevel); + + verify(mockZoomLevelFeature, times(1)).setValue(zoomLevel); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setZoomLevel_shouldUpdateBuilder() throws CameraAccessException { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + float zoomLevel = 1.0f; + + when(mockZoomLevelFeature.getValue()).thenReturn(zoomLevel); + when(mockZoomLevelFeature.getMinimumZoomLevel()).thenReturn(0f); + when(mockZoomLevelFeature.getMaximumZoomLevel()).thenReturn(2f); + + camera.setZoomLevel(mockResult, zoomLevel); + + verify(mockZoomLevelFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setZoomLevel_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + float zoomLevel = 1.0f; + + when(mockZoomLevelFeature.getValue()).thenReturn(zoomLevel); + when(mockZoomLevelFeature.getMinimumZoomLevel()).thenReturn(0f); + when(mockZoomLevelFeature.getMaximumZoomLevel()).thenReturn(2f); + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setZoomLevel(mockResult, zoomLevel); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)).error("setZoomLevelFailed", "Could not set zoom level.", null); + } + + @Test + public void pauseVideoRecording_shouldSendNullResultWhenNotRecording() { + TestUtils.setPrivateField(camera, "recordingVideo", false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.pauseVideoRecording(mockResult); + + verify(mockResult, times(1)).success(null); + verify(mockResult, never()).error(any(), any(), any()); + } + + @Test + public void pauseVideoRecording_shouldCallPauseWhenRecordingAndOnAPIN() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24); + + camera.pauseVideoRecording(mockResult); + + verify(mockMediaRecorder, times(1)).pause(); + verify(mockResult, times(1)).success(null); + verify(mockResult, never()).error(any(), any(), any()); + } + + @Test + public void pauseVideoRecording_shouldSendVideoRecordingFailedErrorWhenVersionCodeSmallerThenN() { + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 23); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.pauseVideoRecording(mockResult); + + verify(mockResult, times(1)) + .error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); + verify(mockResult, never()).success(any()); + } + + @Test + public void + pauseVideoRecording_shouldSendVideoRecordingFailedErrorWhenMediaRecorderPauseThrowsIllegalStateException() { + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24); + + IllegalStateException expectedException = new IllegalStateException("Test error message"); + + doThrow(expectedException).when(mockMediaRecorder).pause(); + + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.pauseVideoRecording(mockResult); + + verify(mockResult, times(1)).error("videoRecordingFailed", "Test error message", null); + verify(mockResult, never()).success(any()); + } + + @Test + public void resumeVideoRecording_shouldSendNullResultWhenNotRecording() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + TestUtils.setPrivateField(camera, "recordingVideo", false); + + camera.resumeVideoRecording(mockResult); + + verify(mockResult, times(1)).success(null); + verify(mockResult, never()).error(any(), any(), any()); + } + + @Test + public void resumeVideoRecording_shouldCallPauseWhenRecordingAndOnAPIN() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24); + + camera.resumeVideoRecording(mockResult); + + verify(mockMediaRecorder, times(1)).resume(); + verify(mockResult, times(1)).success(null); + verify(mockResult, never()).error(any(), any(), any()); + } + + @Test + public void + resumeVideoRecording_shouldSendVideoRecordingFailedErrorWhenVersionCodeSmallerThanN() { + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 23); + + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.resumeVideoRecording(mockResult); + + verify(mockResult, times(1)) + .error("videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); + verify(mockResult, never()).success(any()); + } + + @Test + public void + resumeVideoRecording_shouldSendVideoRecordingFailedErrorWhenMediaRecorderPauseThrowsIllegalStateException() { + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24); + + IllegalStateException expectedException = new IllegalStateException("Test error message"); + + doThrow(expectedException).when(mockMediaRecorder).resume(); + + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.resumeVideoRecording(mockResult); + + verify(mockResult, times(1)).error("videoRecordingFailed", "Test error message", null); + verify(mockResult, never()).success(any()); + } + + @Test + public void setFocusMode_shouldUpdateAutoFocusFeature() { + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.setFocusMode(mockResult, FocusMode.auto); + + verify(mockAutoFocusFeature, times(1)).setValue(FocusMode.auto); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setFocusMode_shouldUpdateBuilder() { + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.setFocusMode(mockResult, FocusMode.auto); + + verify(mockAutoFocusFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setFocusMode_shouldUnlockAutoFocusForAutoMode() { + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.auto); + verify(mockPreviewRequestBuilder, times(1)) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + verify(mockPreviewRequestBuilder, times(1)) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + } + + @Test + public void setFocusMode_shouldSkipUnlockAutoFocusWhenNullCaptureSession() { + TestUtils.setPrivateField(camera, "captureSession", null); + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.auto); + verify(mockPreviewRequestBuilder, never()) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + verify(mockPreviewRequestBuilder, never()) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + } + + @Test + public void setFocusMode_shouldSendErrorEventOnUnlockAutoFocusCameraAccessException() + throws CameraAccessException { + when(mockCaptureSession.capture(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.auto); + verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any()); + } + + @Test + public void setFocusMode_shouldLockAutoFocusForLockedMode() throws CameraAccessException { + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.locked); + verify(mockPreviewRequestBuilder, times(1)) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START); + verify(mockCaptureSession, times(1)).capture(any(), any(), any()); + verify(mockCaptureSession, times(1)).setRepeatingRequest(any(), any(), any()); + } + + @Test + public void setFocusMode_shouldSkipLockAutoFocusWhenNullCaptureSession() { + TestUtils.setPrivateField(camera, "captureSession", null); + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.locked); + verify(mockPreviewRequestBuilder, never()) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + } + + @Test + public void setFocusMode_shouldSendErrorEventOnLockAutoFocusCameraAccessException() + throws CameraAccessException { + when(mockCaptureSession.capture(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.locked); + verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any()); + } + + @Test + public void setFocusMode_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setFocusMode(mockResult, FocusMode.locked); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)) + .error("setFocusModeFailed", "Error setting focus mode: null", null); + } + + @Test + public void setExposureOffset_shouldUpdateExposureOffsetFeature() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.setExposureOffset(mockResult, 1.0); + + verify(mockExposureOffsetFeature, times(1)).setValue(1.0); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setExposureOffset_shouldAndUpdateBuilder() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.setExposureOffset(mockResult, 1.0); + + verify(mockExposureOffsetFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setExposureOffset_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setExposureOffset(mockResult, 1.0); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)) + .error("setExposureOffsetFailed", "Could not set exposure offset.", null); + } + + @Test + public void lockCaptureOrientation_shouldLockCaptureOrientation() { + final Activity mockActivity = mock(Activity.class); + SensorOrientationFeature mockSensorOrientationFeature = + mockCameraFeatureFactory.createSensorOrientationFeature( + mockCameraProperties, mockActivity, mockDartMessenger); + + camera.lockCaptureOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP); + + verify(mockSensorOrientationFeature, times(1)) + .lockCaptureOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test + public void unlockCaptureOrientation_shouldUnlockCaptureOrientation() { + final Activity mockActivity = mock(Activity.class); + SensorOrientationFeature mockSensorOrientationFeature = + mockCameraFeatureFactory.createSensorOrientationFeature( + mockCameraProperties, mockActivity, mockDartMessenger); + + camera.unlockCaptureOrientation(); + + verify(mockSensorOrientationFeature, times(1)).unlockCaptureOrientation(); + } + + private static class TestCameraFeatureFactory implements CameraFeatureFactory { + private final AutoFocusFeature mockAutoFocusFeature; + private final ExposureLockFeature mockExposureLockFeature; + private final ExposureOffsetFeature mockExposureOffsetFeature; + private final ExposurePointFeature mockExposurePointFeature; + private final FlashFeature mockFlashFeature; + private final FocusPointFeature mockFocusPointFeature; + private final FpsRangeFeature mockFpsRangeFeature; + private final NoiseReductionFeature mockNoiseReductionFeature; + private final ResolutionFeature mockResolutionFeature; + private final SensorOrientationFeature mockSensorOrientationFeature; + private final ZoomLevelFeature mockZoomLevelFeature; + + public TestCameraFeatureFactory() { + this.mockAutoFocusFeature = mock(AutoFocusFeature.class); + this.mockExposureLockFeature = mock(ExposureLockFeature.class); + this.mockExposureOffsetFeature = mock(ExposureOffsetFeature.class); + this.mockExposurePointFeature = mock(ExposurePointFeature.class); + this.mockFlashFeature = mock(FlashFeature.class); + this.mockFocusPointFeature = mock(FocusPointFeature.class); + this.mockFpsRangeFeature = mock(FpsRangeFeature.class); + this.mockNoiseReductionFeature = mock(NoiseReductionFeature.class); + this.mockResolutionFeature = mock(ResolutionFeature.class); + this.mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + this.mockZoomLevelFeature = mock(ZoomLevelFeature.class); + } + + @Override + public AutoFocusFeature createAutoFocusFeature( + @NonNull CameraProperties cameraProperties, boolean recordingVideo) { + return mockAutoFocusFeature; + } + + @Override + public ExposureLockFeature createExposureLockFeature( + @NonNull CameraProperties cameraProperties) { + return mockExposureLockFeature; + } + + @Override + public ExposureOffsetFeature createExposureOffsetFeature( + @NonNull CameraProperties cameraProperties) { + return mockExposureOffsetFeature; + } + + @Override + public FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties) { + return mockFlashFeature; + } + + @Override + public ResolutionFeature createResolutionFeature( + @NonNull CameraProperties cameraProperties, + ResolutionPreset initialSetting, + String cameraName) { + return mockResolutionFeature; + } + + @Override + public FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrienttionFeature) { + return mockFocusPointFeature; + } + + @Override + public FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties) { + return mockFpsRangeFeature; + } + + @Override + public SensorOrientationFeature createSensorOrientationFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger) { + return mockSensorOrientationFeature; + } + + @Override + public ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties) { + return mockZoomLevelFeature; + } + + @Override + public ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { + return mockExposurePointFeature; + } + + @Override + public NoiseReductionFeature createNoiseReductionFeature( + @NonNull CameraProperties cameraProperties) { + return mockNoiseReductionFeature; + } + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java index b97192b889cf..6b714ce41e34 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java @@ -12,7 +12,7 @@ public class CameraUtilsTest { @Test - public void serializeDeviceOrientation_serializes_correctly() { + public void serializeDeviceOrientation_serializesCorrectly() { assertEquals( "portraitUp", CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP)); @@ -33,7 +33,7 @@ public void serializeDeviceOrientation_throws_for_null() { } @Test - public void deserializeDeviceOrientation_deserializes_correctly() { + public void deserializeDeviceOrientation_deserializesCorrectly() { assertEquals( PlatformChannel.DeviceOrientation.PORTRAIT_UP, CameraUtils.deserializeDeviceOrientation("portraitUp")); @@ -49,54 +49,7 @@ public void deserializeDeviceOrientation_deserializes_correctly() { } @Test(expected = UnsupportedOperationException.class) - public void deserializeDeviceOrientation_throws_for_null() { + public void deserializeDeviceOrientation_throwsForNull() { CameraUtils.deserializeDeviceOrientation(null); } - - @Test - public void getDeviceOrientationFromDegrees_converts_correctly() { - // Portrait UP - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(0)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(315)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(44)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(-45)); - // Portrait DOWN - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.getDeviceOrientationFromDegrees(180)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.getDeviceOrientationFromDegrees(135)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.getDeviceOrientationFromDegrees(224)); - // Landscape LEFT - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.getDeviceOrientationFromDegrees(90)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.getDeviceOrientationFromDegrees(45)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.getDeviceOrientationFromDegrees(134)); - // Landscape RIGHT - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.getDeviceOrientationFromDegrees(270)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.getDeviceOrientationFromDegrees(225)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.getDeviceOrientationFromDegrees(314)); - } } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java index 1385c2e36949..d3e495551608 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java @@ -19,7 +19,7 @@ public class CameraZoomTest { @Test - public void ctor_when_parameters_are_valid() { + public void ctor_whenParametersAreValid() { final Rect sensorSize = new Rect(0, 0, 0, 0); final Float maxZoom = 4.0f; final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); @@ -31,7 +31,7 @@ public void ctor_when_parameters_are_valid() { } @Test - public void ctor_when_sensor_size_is_null() { + public void ctor_whenSensorSizeIsNull() { final Rect sensorSize = null; final Float maxZoom = 4.0f; final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); @@ -42,7 +42,7 @@ public void ctor_when_sensor_size_is_null() { } @Test - public void ctor_when_max_zoom_is_null() { + public void ctor_whenMaxZoomIsNull() { final Rect sensorSize = new Rect(0, 0, 0, 0); final Float maxZoom = null; final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); @@ -53,7 +53,7 @@ public void ctor_when_max_zoom_is_null() { } @Test - public void ctor_when_max_zoom_is_smaller_then_default_zoom_factor() { + public void ctor_whenMaxZoomIsSmallerThenDefaultZoomFactor() { final Rect sensorSize = new Rect(0, 0, 0, 0); final Float maxZoom = 0.5f; final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); @@ -64,7 +64,7 @@ public void ctor_when_max_zoom_is_smaller_then_default_zoom_factor() { } @Test - public void setZoom_when_no_support_should_not_set_scaler_crop_region() { + public void setZoom_whenNoSupportShouldNotSetScalerCropRegion() { final CameraZoom cameraZoom = new CameraZoom(null, null); final Rect computedZoom = cameraZoom.computeZoom(2f); @@ -72,7 +72,7 @@ public void setZoom_when_no_support_should_not_set_scaler_crop_region() { } @Test - public void setZoom_when_sensor_size_equals_zero_should_return_crop_region_of_zero() { + public void setZoom_whenSensorSizeEqualsZeroShouldReturnCropRegionOfZero() { final Rect sensorSize = new Rect(0, 0, 0, 0); final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f); final Rect computedZoom = cameraZoom.computeZoom(18f); @@ -85,7 +85,7 @@ public void setZoom_when_sensor_size_equals_zero_should_return_crop_region_of_ze } @Test - public void setZoom_when_sensor_size_is_valid_should_return_crop_region() { + public void setZoom_whenSensorSizeIsValidShouldReturnCropRegion() { final Rect sensorSize = new Rect(0, 0, 100, 100); final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f); final Rect computedZoom = cameraZoom.computeZoom(18f); @@ -98,7 +98,7 @@ public void setZoom_when_sensor_size_is_valid_should_return_crop_region() { } @Test - public void setZoom_when_zoom_is_greater_then_max_zoom_clamp_to_max_zoom() { + public void setZoom_whenZoomIsGreaterThenMaxZoomClampToMaxZoom() { final Rect sensorSize = new Rect(0, 0, 100, 100); final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f); final Rect computedZoom = cameraZoom.computeZoom(25f); @@ -111,7 +111,7 @@ public void setZoom_when_zoom_is_greater_then_max_zoom_clamp_to_max_zoom() { } @Test - public void setZoom_when_zoom_is_smaller_then_min_zoom_clamp_to_min_zoom() { + public void setZoom_whenZoomIsSmallerThenMinZoomClampToMinZoom() { final Rect sensorSize = new Rect(0, 0, 100, 100); final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f); final Rect computedZoom = cameraZoom.computeZoom(0.5f); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java index 25f5df9e9db9..0a2fc43d03cb 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java @@ -16,8 +16,8 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.StandardMethodCodec; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java index d2c9f4498332..0358ce6cb785 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java @@ -80,7 +80,7 @@ public void teardown() { } @Test - public void run_writes_bytes_to_file_and_finishes_with_path() throws IOException { + public void runWritesBytesToFileAndFinishesWithPath() throws IOException { imageSaver.run(); verify(mockFileOutputStream, times(1)).write(new byte[] {0x42, 0x00, 0x13}); @@ -89,7 +89,7 @@ public void run_writes_bytes_to_file_and_finishes_with_path() throws IOException } @Test - public void run_calls_error_on_write_ioexception() throws IOException { + public void runCallsErrorOnWriteIoexception() throws IOException { doThrow(new IOException()).when(mockFileOutputStream).write(any()); imageSaver.run(); verify(mockCallback, times(1)).onError("IOError", "Failed saving image"); @@ -97,7 +97,7 @@ public void run_calls_error_on_write_ioexception() throws IOException { } @Test - public void run_calls_error_on_close_ioexception() throws IOException { + public void runCallsErrorOnCloseIoexception() throws IOException { doThrow(new IOException("message")).when(mockFileOutputStream).close(); imageSaver.run(); verify(mockCallback, times(1)).onError("cameraAccess", "message"); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java deleted file mode 100644 index f257a7f7fd4b..000000000000 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.camera; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import io.flutter.plugin.common.MethodChannel; -import org.junit.Test; - -public class PictureCaptureRequestTest { - - @Test - public void state_is_idle_by_default() { - PictureCaptureRequest req = new PictureCaptureRequest(null); - assertEquals("Default state is idle", req.getState(), PictureCaptureRequest.State.idle); - } - - @Test - public void setState_sets_state() { - PictureCaptureRequest req = new PictureCaptureRequest(null); - req.setState(PictureCaptureRequest.State.focusing); - assertEquals("State is focusing", req.getState(), PictureCaptureRequest.State.focusing); - req.setState(PictureCaptureRequest.State.preCapture); - assertEquals("State is preCapture", req.getState(), PictureCaptureRequest.State.preCapture); - req.setState(PictureCaptureRequest.State.waitingPreCaptureReady); - assertEquals( - "State is waitingPreCaptureReady", - req.getState(), - PictureCaptureRequest.State.waitingPreCaptureReady); - req.setState(PictureCaptureRequest.State.capturing); - assertEquals( - "State is awaitingPreCapture", req.getState(), PictureCaptureRequest.State.capturing); - } - - @Test - public void setState_resets_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - PictureCaptureRequest req = new PictureCaptureRequest(null, mockTimeoutHandler); - req.setState(PictureCaptureRequest.State.focusing); - req.setState(PictureCaptureRequest.State.preCapture); - req.setState(PictureCaptureRequest.State.waitingPreCaptureReady); - req.setState(PictureCaptureRequest.State.capturing); - verify(mockTimeoutHandler, times(4)).resetTimeout(any()); - verify(mockTimeoutHandler, never()).clearTimeout(any()); - } - - @Test - public void setState_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - PictureCaptureRequest req = new PictureCaptureRequest(null, mockTimeoutHandler); - req.setState(PictureCaptureRequest.State.idle); - req.setState(PictureCaptureRequest.State.finished); - req = new PictureCaptureRequest(null, mockTimeoutHandler); - req.setState(PictureCaptureRequest.State.error); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler, times(3)).clearTimeout(any()); - } - - @Test - public void finish_sets_result_and_state() { - // Setup - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult); - // Act - req.finish("/test/path"); - // Test - verify(mockResult).success("/test/path"); - assertEquals("State is finished", req.getState(), PictureCaptureRequest.State.finished); - } - - @Test - public void finish_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, mockTimeoutHandler); - req.finish("/test/path"); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); - } - - @Test - public void isFinished_is_true_When_state_is_finished_or_error() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null); - // Test false states - req.setState(PictureCaptureRequest.State.idle); - assertFalse(req.isFinished()); - req.setState(PictureCaptureRequest.State.preCapture); - assertFalse(req.isFinished()); - req.setState(PictureCaptureRequest.State.capturing); - assertFalse(req.isFinished()); - // Test true states - req.setState(PictureCaptureRequest.State.finished); - assertTrue(req.isFinished()); - req = new PictureCaptureRequest(null); // Refresh - req.setState(PictureCaptureRequest.State.error); - assertTrue(req.isFinished()); - } - - @Test(expected = IllegalStateException.class) - public void finish_throws_When_already_finished() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null); - req.setState(PictureCaptureRequest.State.finished); - // Act - req.finish("/test/path"); - } - - @Test - public void error_sets_result_and_state() { - // Setup - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult); - // Act - req.error("ERROR_CODE", "Error Message", null); - // Test - verify(mockResult).error("ERROR_CODE", "Error Message", null); - assertEquals("State is error", req.getState(), PictureCaptureRequest.State.error); - } - - @Test - public void error_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, mockTimeoutHandler); - req.error("ERROR_CODE", "Error Message", null); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); - } - - @Test(expected = IllegalStateException.class) - public void error_throws_When_already_finished() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null); - req.setState(PictureCaptureRequest.State.finished); - // Act - req.error(null, null, null); - } -} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java index 84e4ad0d0e91..fd8ef7c766a2 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java @@ -28,7 +28,7 @@ public class AutoFocusFeatureTest { }; @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -36,7 +36,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { } @Test - public void getValue_should_return_auto_if_not_set() { + public void getValue_shouldReturnAutoIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -44,7 +44,7 @@ public void getValue_should_return_auto_if_not_set() { } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); FocusMode expectedValue = FocusMode.locked; @@ -56,7 +56,7 @@ public void getValue_should_echo_the_set_value() { } @Test - public void checkIsSupported_should_return_false_when_minimum_focus_distance_is_zero() { + public void checkIsSupported_shouldReturnFalseWhenMinimumFocusDistanceIsZero() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -67,7 +67,7 @@ public void checkIsSupported_should_return_false_when_minimum_focus_distance_is_ } @Test - public void checkIsSupported_should_return_false_when_minimum_focus_distance_is_null() { + public void checkIsSupported_shouldReturnFalseWhenMinimumFocusDistanceIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -78,7 +78,7 @@ public void checkIsSupported_should_return_false_when_minimum_focus_distance_is_ } @Test - public void checkIsSupport_should_return_false_when_no_focus_modes_are_available() { + public void checkIsSupport_shouldReturnFalseWhenNoFocusModesAreAvailable() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -89,7 +89,7 @@ public void checkIsSupport_should_return_false_when_no_focus_modes_are_available } @Test - public void checkIsSupport_should_return_false_when_only_focus_off_is_available() { + public void checkIsSupport_shouldReturnFalseWhenOnlyFocusOffIsAvailable() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -100,7 +100,7 @@ public void checkIsSupport_should_return_false_when_only_focus_off_is_available( } @Test - public void checkIsSupport_should_return_true_when_only_multiple_focus_modes_are_available() { + public void checkIsSupport_shouldReturnTrueWhenOnlyMultipleFocusModesAreAvailable() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -111,7 +111,7 @@ public void checkIsSupport_should_return_true_when_only_multiple_focus_modes_are } @Test - public void updateBuilder_should_return_when_checkIsSupported_is_false() { + public void updateBuilderShouldReturnWhenCheckIsSupportedIsFalse() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -125,7 +125,7 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void updateBuilder_should_set_control_mode_to_auto_when_focus_is_locked() { + public void updateBuilder_shouldSetControlModeToAutoWhenFocusIsLocked() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -142,7 +142,7 @@ public void updateBuilder_should_set_control_mode_to_auto_when_focus_is_locked() @Test public void - updateBuilder_should_set_control_mode_to_continuous_video_when_focus_is_auto_and_recording_video() { + updateBuilder_shouldSetControlModeToContinuousVideoWhenFocusIsAutoAndRecordingVideo() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, true); @@ -159,7 +159,7 @@ public void updateBuilder_should_set_control_mode_to_auto_when_focus_is_locked() @Test public void - updateBuilder_should_set_control_mode_to_continuous_video_when_focus_is_auto_and_not_recording_video() { + updateBuilder_shouldSetControlModeToContinuousVideoWhenFocusIsAutoAndNotRecordingVideo() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java index 70d52d458d4d..f68ae7140601 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java @@ -11,7 +11,7 @@ public class FocusModeTest { @Test - public void getValueForString_returns_correct_values() { + public void getValueForString_returnsCorrectValues() { assertEquals( "Returns FocusMode.auto for 'auto'", FocusMode.getValueForString("auto"), FocusMode.auto); assertEquals( @@ -21,13 +21,13 @@ public void getValueForString_returns_correct_values() { } @Test - public void getValueForString_returns_null_for_nonexistant_value() { + public void getValueForString_returnsNullForNonexistantValue() { assertEquals( "Returns null for 'nonexistant'", FocusMode.getValueForString("nonexistant"), null); } @Test - public void toString_returns_correct_value() { + public void toString_returnsCorrectValue() { assertEquals("Returns 'auto' for FocusMode.auto", FocusMode.auto.toString(), "auto"); assertEquals("Returns 'locked' for FocusMode.locked", FocusMode.locked.toString(), "locked"); } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java index d9e0a8d69c96..1cda0a86d575 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java @@ -16,7 +16,7 @@ public class ExposureLockFeatureTest { @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); @@ -24,7 +24,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { } @Test - public void getValue_should_return_auto_if_not_set() { + public void getValue_shouldReturnAutoIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); @@ -32,7 +32,7 @@ public void getValue_should_return_auto_if_not_set() { } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); ExposureMode expectedValue = ExposureMode.locked; @@ -44,7 +44,7 @@ public void getValue_should_echo_the_set_value() { } @Test - public void checkIsSupported_should_return_true() { + public void checkIsSupported_shouldReturnTrue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); @@ -52,8 +52,7 @@ public void checkIsSupported_should_return_true() { } @Test - public void - updateBuilder_should_set_control_ae_lock_to_false_when_auto_exposure_is_set_to_auto() { + public void updateBuilder_shouldSetControlAeLockToFalseWhenAutoExposureIsSetToAuto() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); @@ -65,8 +64,7 @@ public void checkIsSupported_should_return_true() { } @Test - public void - updateBuilder_should_set_control_ae_lock_to_false_when_auto_exposure_is_set_to_locked() { + public void updateBuilder_shouldSetControlAeLockToFalseWhenAutoExposureIsSetToLocked() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java index ad1d3d98f295..d5d47697776c 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java @@ -11,7 +11,7 @@ public class ExposureModeTest { @Test - public void getValueForString_returns_correct_values() { + public void getValueForString_returnsCorrectValues() { assertEquals( "Returns ExposureMode.auto for 'auto'", ExposureMode.getValueForString("auto"), @@ -23,13 +23,13 @@ public void getValueForString_returns_correct_values() { } @Test - public void getValueForString_returns_null_for_nonexistant_value() { + public void getValueForString_returnsNullForNonexistantValue() { assertEquals( "Returns null for 'nonexistant'", ExposureMode.getValueForString("nonexistant"), null); } @Test - public void toString_returns_correct_value() { + public void toString_returnsCorrectValue() { assertEquals("Returns 'auto' for ExposureMode.auto", ExposureMode.auto.toString(), "auto"); assertEquals( "Returns 'locked' for ExposureMode.locked", ExposureMode.locked.toString(), "locked"); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java index 40d17fdc496e..ee428f3d5e02 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java @@ -17,7 +17,7 @@ public class ExposureOffsetFeatureTest { @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); @@ -25,7 +25,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { } @Test - public void getValue_should_return_zero_if_not_set() { + public void getValue_shouldReturnZeroIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); @@ -35,7 +35,7 @@ public void getValue_should_return_zero_if_not_set() { } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); double expectedValue = 4.0; @@ -49,8 +49,7 @@ public void getValue_should_echo_the_set_value() { } @Test - public void - getExposureOffsetStepSize_should_return_the_control_exposure_compensation_step_value() { + public void getExposureOffsetStepSize_shouldReturnTheControlExposureCompensationStepValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); @@ -60,7 +59,7 @@ public void getValue_should_echo_the_set_value() { } @Test - public void checkIsSupported_should_return_true() { + public void checkIsSupported_shouldReturnTrue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); @@ -68,7 +67,7 @@ public void checkIsSupported_should_return_true() { } @Test - public void updateBuilder_should_set_control_ae_exposure_compensation_to_offset() { + public void updateBuilder_shouldSetControlAeExposureCompensationToOffset() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java index 4a515c6fd0ec..b34a04fe26b7 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java @@ -19,9 +19,12 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; @@ -30,35 +33,44 @@ public class ExposurePointFeatureTest { Size mockCameraBoundaries; + SensorOrientationFeature mockSensorOrientationFeature; + DeviceOrientationManager mockDeviceOrientationManager; @Before public void setUp() { this.mockCameraBoundaries = mock(Size.class); when(this.mockCameraBoundaries.getWidth()).thenReturn(100); when(this.mockCameraBoundaries.getHeight()).thenReturn(100); + mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + when(mockSensorOrientationFeature.getDeviceOrientationManager()) + .thenReturn(mockDeviceOrientationManager); + when(mockDeviceOrientationManager.getLastUIOrientation()) + .thenReturn(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); } @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - CameraRegionUtils mockCameraRegions = mock(CameraRegionUtils.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); assertEquals("ExposurePointFeature", exposurePointFeature.getDebugName()); } @Test - public void getValue_should_return_null_if_not_set() { + public void getValue_shouldReturnNullIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); - Point actualPoint = exposurePointFeature.getValue(); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); assertNull(exposurePointFeature.getValue()); } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point expectedPoint = new Point(0.0, 0.0); @@ -69,9 +81,10 @@ public void getValue_should_echo_the_set_value() { } @Test - public void setValue_should_reset_point_when_x_coord_is_null() { + public void setValue_shouldResetPointWhenXCoordIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(new Point(null, 0.0)); @@ -80,9 +93,10 @@ public void setValue_should_reset_point_when_x_coord_is_null() { } @Test - public void setValue_should_reset_point_when_y_coord_is_null() { + public void setValue_shouldResetPointWhenYCoordIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(new Point(0.0, null)); @@ -91,9 +105,10 @@ public void setValue_should_reset_point_when_y_coord_is_null() { } @Test - public void setValue_should_set_point_when_valid_coords_are_supplied() { + public void setValue_shouldSetPointWhenValidCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point point = new Point(0.0, 0.0); @@ -103,11 +118,11 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { } @Test - public void - setValue_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + public void setValue_shouldDetermineMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); @@ -117,16 +132,22 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { exposurePointFeature.setValue(new Point(0.5, 0.5)); mockedCameraRegionUtils.verify( - () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), times(1)); } } @Test(expected = AssertionError.class) - public void setValue_should_throw_assertion_error_when_no_valid_boundaries_are_set() { + public void setValue_shouldThrowAssertionErrorWhenNoValidBoundariesAreSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); try (MockedStatic mockedCameraRegionUtils = Mockito.mockStatic(CameraRegionUtils.class)) { @@ -135,10 +156,11 @@ public void setValue_should_throw_assertion_error_when_no_valid_boundaries_are_s } @Test - public void setValue_should_not_determine_metering_rectangle_when_null_coords_are_set() { + public void setValue_shouldNotDetermineMeteringRectangleWhenNullCoordsAreSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); @@ -155,10 +177,11 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar @Test public void - setCameraBoundaries_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + setCameraBoundaries_shouldDetermineMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(new Point(0.5, 0.5)); Size mockedCameraBoundaries = mock(Size.class); @@ -169,15 +192,21 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); mockedCameraRegionUtils.verify( - () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), times(1)); } } @Test - public void checkIsSupported_should_return_false_when_max_regions_is_null() { + public void checkIsSupported_shouldReturnFalseWhenMaxRegionsIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(null); @@ -186,9 +215,10 @@ public void checkIsSupported_should_return_false_when_max_regions_is_null() { } @Test - public void checkIsSupported_should_return_false_when_max_regions_is_zero() { + public void checkIsSupported_shouldReturnFalseWhenMaxRegionsIsZero() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); @@ -197,9 +227,10 @@ public void checkIsSupported_should_return_false_when_max_regions_is_zero() { } @Test - public void checkIsSupported_should_return_true_when_max_regions_is_bigger_then_zero() { + public void checkIsSupported_shouldReturnTrueWhenMaxRegionsIsBiggerThenZero() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); @@ -208,10 +239,11 @@ public void checkIsSupported_should_return_true_when_max_regions_is_bigger_then_ } @Test - public void updateBuilder_should_return_when_checkIsSupported_is_false() { + public void updateBuilder_shouldReturnWhenCheckIsSupportedIsFalse() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); @@ -221,12 +253,12 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void - updateBuilder_should_set_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + public void updateBuilder_shouldSetMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); @@ -236,7 +268,10 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { .when( () -> CameraRegionUtils.convertPointToMeteringRectangle( - mockedCameraBoundaries, 0.5, 0.5)) + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)) .thenReturn(mockedMeteringRectangle); exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); exposurePointFeature.setValue(new Point(0.5, 0.5)); @@ -249,13 +284,12 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void - updateBuilder_should_not_set_metering_rectangle_when_no_valid_boundaries_are_supplied() { + public void updateBuilder_shouldNotSetMeteringRectangleWhenNoValidBoundariesAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); - MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); @@ -263,11 +297,12 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void updateBuilder_should_not_set_metering_rectangle_when_no_valid_coords_are_supplied() { + public void updateBuilder_shouldNotSetMeteringRectangleWhenNoValidCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(null); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java index eccfb07993c1..f2b4ffc8197c 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java @@ -20,7 +20,7 @@ public class FlashFeatureTest { @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -28,7 +28,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { } @Test - public void getValue_should_return_auto_if_not_set() { + public void getValue_shouldReturnAutoIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -36,7 +36,7 @@ public void getValue_should_return_auto_if_not_set() { } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); FlashMode expectedValue = FlashMode.torch; @@ -48,7 +48,7 @@ public void getValue_should_echo_the_set_value() { } @Test - public void checkIsSupported_should_return_false_when_flash_info_available_is_null() { + public void checkIsSupported_shouldReturnFalseWhenFlashInfoAvailableIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -58,7 +58,7 @@ public void checkIsSupported_should_return_false_when_flash_info_available_is_nu } @Test - public void checkIsSupported_should_return_false_when_flash_info_available_is_false() { + public void checkIsSupported_shouldReturnFalseWhenFlashInfoAvailableIsFalse() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -68,7 +68,7 @@ public void checkIsSupported_should_return_false_when_flash_info_available_is_fa } @Test - public void checkIsSupported_should_return_true_when_flash_info_available_is_true() { + public void checkIsSupported_shouldReturnTrueWhenFlashInfoAvailableIsTrue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -78,7 +78,7 @@ public void checkIsSupported_should_return_true_when_flash_info_available_is_tru } @Test - public void updateBuilder_should_return_when_checkIsSupported_is_false() { + public void updateBuilder_shouldReturnWhenCheckIsSupportedIsFalse() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -91,7 +91,7 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_off() { + public void updateBuilder_shouldSetAeModeAndFlashModeWhenFlashModeIsOff() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -107,7 +107,7 @@ public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_o } @Test - public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_always() { + public void updateBuilder_shouldSetAeModeAndFlashModeWhenFlashModeIsAlways() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -123,7 +123,7 @@ public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_a } @Test - public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_torch() { + public void updateBuilder_shouldSetAeModeAndFlashModeWhenFlashModeIsTorch() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -139,7 +139,7 @@ public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_t } @Test - public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_auto() { + public void updateBuilder_shouldSetAeModeAndFlashModeWhenFlashModeIsAuto() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java index d158336ef235..f03dc9f62e87 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java @@ -19,9 +19,12 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; @@ -30,35 +33,45 @@ public class FocusPointFeatureTest { Size mockCameraBoundaries; + SensorOrientationFeature mockSensorOrientationFeature; + DeviceOrientationManager mockDeviceOrientationManager; @Before public void setUp() { this.mockCameraBoundaries = mock(Size.class); when(this.mockCameraBoundaries.getWidth()).thenReturn(100); when(this.mockCameraBoundaries.getHeight()).thenReturn(100); + mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + when(mockSensorOrientationFeature.getDeviceOrientationManager()) + .thenReturn(mockDeviceOrientationManager); + when(mockDeviceOrientationManager.getLastUIOrientation()) + .thenReturn(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); } @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - CameraRegionUtils mockCameraRegions = mock(CameraRegionUtils.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); assertEquals("FocusPointFeature", focusPointFeature.getDebugName()); } @Test - public void getValue_should_return_null_if_not_set() { + public void getValue_shouldReturnNullIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); Point actualPoint = focusPointFeature.getValue(); assertNull(focusPointFeature.getValue()); } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point expectedPoint = new Point(0.0, 0.0); @@ -69,9 +82,10 @@ public void getValue_should_echo_the_set_value() { } @Test - public void setValue_should_reset_point_when_x_coord_is_null() { + public void setValue_shouldResetPointWhenXCoordIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(new Point(null, 0.0)); @@ -80,9 +94,10 @@ public void setValue_should_reset_point_when_x_coord_is_null() { } @Test - public void setValue_should_reset_point_when_y_coord_is_null() { + public void setValue_shouldResetPointWhenYCoordIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(new Point(0.0, null)); @@ -91,9 +106,10 @@ public void setValue_should_reset_point_when_y_coord_is_null() { } @Test - public void setValue_should_set_point_when_valid_coords_are_supplied() { + public void setValue_shouldSetPointWhenValidCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point point = new Point(0.0, 0.0); @@ -103,11 +119,11 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { } @Test - public void - setValue_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + public void setValue_shouldDetermineMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); @@ -117,16 +133,22 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { focusPointFeature.setValue(new Point(0.5, 0.5)); mockedCameraRegionUtils.verify( - () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), times(1)); } } @Test(expected = AssertionError.class) - public void setValue_should_throw_assertion_error_when_no_valid_boundaries_are_set() { + public void setValue_shouldThrowAssertionErrorWhenNoValidBoundariesAreSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); try (MockedStatic mockedCameraRegionUtils = Mockito.mockStatic(CameraRegionUtils.class)) { @@ -135,10 +157,11 @@ public void setValue_should_throw_assertion_error_when_no_valid_boundaries_are_s } @Test - public void setValue_should_not_determine_metering_rectangle_when_null_coords_are_set() { + public void setValue_shouldNotDetermineMeteringRectangleWhenNullCoordsAreSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); @@ -155,10 +178,11 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar @Test public void - setCameraBoundaries_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + setCameraBoundaries_shouldDetermineMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(new Point(0.5, 0.5)); Size mockedCameraBoundaries = mock(Size.class); @@ -169,15 +193,21 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); mockedCameraRegionUtils.verify( - () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), times(1)); } } @Test - public void checkIsSupported_should_return_false_when_max_regions_is_null() { + public void checkIsSupported_shouldReturnFalseWhenMaxRegionsIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(null); @@ -186,9 +216,10 @@ public void checkIsSupported_should_return_false_when_max_regions_is_null() { } @Test - public void checkIsSupported_should_return_false_when_max_regions_is_zero() { + public void checkIsSupported_shouldReturnFalseWhenMaxRegionsIsZero() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(0); @@ -197,9 +228,10 @@ public void checkIsSupported_should_return_false_when_max_regions_is_zero() { } @Test - public void checkIsSupported_should_return_true_when_max_regions_is_bigger_then_zero() { + public void checkIsSupported_shouldReturnTrueWhenMaxRegionsIsBiggerThenZero() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); @@ -208,10 +240,11 @@ public void checkIsSupported_should_return_true_when_max_regions_is_bigger_then_ } @Test - public void updateBuilder_should_return_when_checkIsSupported_is_false() { + public void updateBuilder_shouldReturnWhenCheckIsSupportedIsFalse() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(0); @@ -221,12 +254,12 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void - updateBuilder_should_set_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + public void updateBuilder_shouldSetMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); @@ -236,7 +269,10 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { .when( () -> CameraRegionUtils.convertPointToMeteringRectangle( - mockedCameraBoundaries, 0.5, 0.5)) + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)) .thenReturn(mockedMeteringRectangle); focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); focusPointFeature.setValue(new Point(0.5, 0.5)); @@ -249,12 +285,12 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void - updateBuilder_should_not_set_metering_rectangle_when_no_valid_boundaries_are_supplied() { + public void updateBuilder_shouldNotSetMeteringRectangleWhenNoValidBoundariesAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); focusPointFeature.updateBuilder(mockCaptureRequestBuilder); @@ -263,11 +299,12 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void updateBuilder_should_not_set_metering_rectangle_when_no_valid_coords_are_supplied() { + public void updateBuilder_shouldNotSetMeteringRectangleWhenNoValidCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(null); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java index 7b6e70fff5b2..93cfe5523df3 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java @@ -18,7 +18,7 @@ @RunWith(RobolectricTestRunner.class) public class FpsRangeFeaturePixel4aTest { @Test - public void ctor_should_initialize_fps_range_with_30_when_device_is_pixel_4a() { + public void ctor_shouldInitializeFpsRangeWith30WhenDeviceIsPixel4a() { TestUtils.setFinalStatic(Build.class, "BRAND", "google"); TestUtils.setFinalStatic(Build.class, "MODEL", "Pixel 4a"); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java index 77937b5e87c6..2bb4d849a277 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java @@ -35,19 +35,19 @@ public void after() { } @Test - public void ctor_should_initialize_fps_range_with_highest_upper_value_from_range_array() { + public void ctor_shouldInitializeFpsRangeWithHighestUpperValueFromRangeArray() { FpsRangeFeature fpsRangeFeature = createTestInstance(); assertEquals(13, (int) fpsRangeFeature.getValue().getUpper()); } @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { FpsRangeFeature fpsRangeFeature = createTestInstance(); assertEquals("FpsRangeFeature", fpsRangeFeature.getDebugName()); } @Test - public void getValue_should_return_highest_upper_range_if_not_set() { + public void getValue_shouldReturnHighestUpperRangeIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FpsRangeFeature fpsRangeFeature = createTestInstance(); @@ -55,7 +55,7 @@ public void getValue_should_return_highest_upper_range_if_not_set() { } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FpsRangeFeature fpsRangeFeature = new FpsRangeFeature(mockCameraProperties); @SuppressWarnings("unchecked") @@ -68,14 +68,14 @@ public void getValue_should_echo_the_set_value() { } @Test - public void checkIsSupported_should_return_true() { + public void checkIsSupported_shouldReturnTrue() { FpsRangeFeature fpsRangeFeature = createTestInstance(); assertTrue(fpsRangeFeature.checkIsSupported()); } @Test @SuppressWarnings("unchecked") - public void updateBuilder_should_set_ae_target_fps_range() { + public void updateBuilder_shouldSetAeTargetFpsRange() { CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); FpsRangeFeature fpsRangeFeature = createTestInstance(); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java index eb1a639a2ac3..b89aad0f6773 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java @@ -37,7 +37,7 @@ public void after() { } @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); @@ -45,7 +45,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { } @Test - public void getValue_should_return_fast_if_not_set() { + public void getValue_shouldReturnFastIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); @@ -53,7 +53,7 @@ public void getValue_should_return_fast_if_not_set() { } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); NoiseReductionMode expectedValue = NoiseReductionMode.fast; @@ -65,7 +65,7 @@ public void getValue_should_echo_the_set_value() { } @Test - public void checkIsSupported_should_return_false_when_available_noise_reduction_modes_is_null() { + public void checkIsSupported_shouldReturnFalseWhenAvailableNoiseReductionModesIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); @@ -76,7 +76,7 @@ public void checkIsSupported_should_return_false_when_available_noise_reduction_ @Test public void - checkIsSupported_should_return_false_when_available_noise_reduction_modes_returns_an_empty_array() { + checkIsSupported_shouldReturnFalseWhenAvailableNoiseReductionModesReturnsAnEmptyArray() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); @@ -87,7 +87,7 @@ public void checkIsSupported_should_return_false_when_available_noise_reduction_ @Test public void - checkIsSupported_should_return_true_when_available_noise_reduction_modes_returns_at_least_one_item() { + checkIsSupported_shouldReturnTrueWhenAvailableNoiseReductionModesReturnsAtLeastOneItem() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); @@ -97,7 +97,7 @@ public void checkIsSupported_should_return_false_when_available_noise_reduction_ } @Test - public void updateBuilder_should_return_when_checkIsSupported_is_false() { + public void updateBuilder_shouldReturnWhenCheckIsSupportedIsFalse() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); @@ -110,29 +110,28 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void updateBuilder_should_set_noise_reduction_mode_off_when_off() { + public void updateBuilder_shouldSetNoiseReductionModeOffWhenOff() { testUpdateBuilderWith(NoiseReductionMode.off, CaptureRequest.NOISE_REDUCTION_MODE_OFF); } @Test - public void updateBuilder_should_set_noise_reduction_mode_fast_when_fast() { + public void updateBuilder_shouldSetNoiseReductionModeFastWhenFast() { testUpdateBuilderWith(NoiseReductionMode.fast, CaptureRequest.NOISE_REDUCTION_MODE_FAST); } @Test - public void updateBuilder_should_set_noise_reduction_mode_high_quality_when_high_quality() { + public void updateBuilder_shouldSetNoiseReductionModeHighQualityWhenHighQuality() { testUpdateBuilderWith( NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY); } @Test - public void updateBuilder_should_set_noise_reduction_mode_minimal_when_minimal() { + public void updateBuilder_shouldSetNoiseReductionModeMinimalWhenMinimal() { testUpdateBuilderWith(NoiseReductionMode.minimal, CaptureRequest.NOISE_REDUCTION_MODE_MINIMAL); } @Test - public void - updateBuilder_should_set_noise_reduction_mode_zero_shutter_lag_when_zero_shutter_lag() { + public void updateBuilder_shouldSetNoiseReductionModeZeroShutterLagWhenZeroShutterLag() { testUpdateBuilderWith( NoiseReductionMode.zeroShutterLag, CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG); } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java index bb9cb61e1508..e09223dfabe9 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java @@ -79,7 +79,7 @@ public void after() { } @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); @@ -88,7 +88,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { } @Test - public void getValue_should_return_initial_value_when_not_set() { + public void getValue_shouldReturnInitialValueWhenNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); @@ -97,7 +97,7 @@ public void getValue_should_return_initial_value_when_not_set() { } @Test - public void getValue_should_echo_setValue() { + public void getValue_shouldEchoSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); @@ -108,7 +108,7 @@ public void getValue_should_echo_setValue() { } @Test - public void checkIsSupport_returns_true() { + public void checkIsSupport_returnsTrue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); @@ -117,7 +117,7 @@ public void checkIsSupport_returns_true() { } @Test - public void getBestAvailableCamcorderProfileForResolutionPreset_should_fall_through() { + public void getBestAvailableCamcorderProfileForResolutionPreset_shouldFallThrough() { mockedStaticProfile .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) .thenReturn(false); @@ -147,42 +147,42 @@ public void getBestAvailableCamcorderProfileForResolutionPreset_should_fall_thro } @Test - public void computeBestPreviewSize_should_use_720P_when_resolution_preset_max() { + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetMax() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } @Test - public void computeBestPreviewSize_should_use_720P_when_resolution_preset_ultraHigh() { + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetUltraHigh() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.ultraHigh); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } @Test - public void computeBestPreviewSize_should_use_720P_when_resolution_preset_veryHigh() { + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetVeryHigh() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.veryHigh); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } @Test - public void computeBestPreviewSize_should_use_720P_when_resolution_preset_high() { + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetHigh() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.high); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } @Test - public void computeBestPreviewSize_should_use_480P_when_resolution_preset_medium() { + public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMedium() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.medium); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)); } @Test - public void computeBestPreviewSize_should_use_QVGA_when_resolution_preset_low() { + public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLow() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.low); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java index 6e8d04d20e99..58f17cb758bf 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java @@ -50,15 +50,15 @@ public void before() { } @Test - public void getMediaOrientation_when_natural_screen_orientation_equals_portrait_up() { + public void getVideoOrientation_whenNaturalScreenOrientationEqualsPortraitUp() { int degreesPortraitUp = - deviceOrientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_UP); + deviceOrientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_UP); int degreesPortraitDown = - deviceOrientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_DOWN); + deviceOrientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_DOWN); int degreesLandscapeLeft = - deviceOrientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_LEFT); + deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_LEFT); int degreesLandscapeRight = - deviceOrientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); assertEquals(0, degreesPortraitUp); assertEquals(90, degreesLandscapeLeft); @@ -67,17 +67,17 @@ public void getMediaOrientation_when_natural_screen_orientation_equals_portrait_ } @Test - public void getMediaOrientation_when_natural_screen_orientation_equals_landscape_left() { + public void getVideoOrientation_whenNaturalScreenOrientationEqualsLandscapeLeft() { DeviceOrientationManager orientationManager = DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 90); - int degreesPortraitUp = orientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitUp = orientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_UP); int degreesPortraitDown = - orientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_DOWN); + orientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_DOWN); int degreesLandscapeLeft = - orientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_LEFT); + orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_LEFT); int degreesLandscapeRight = - orientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); assertEquals(90, degreesPortraitUp); assertEquals(180, degreesLandscapeLeft); @@ -86,105 +86,96 @@ public void getMediaOrientation_when_natural_screen_orientation_equals_landscape } @Test - public void getMediaOrientation_should_fallback_to_sensor_orientation_when_orientation_is_null() { + public void getVideoOrientation_shouldFallbackToSensorOrientationWhenOrientationIsNull() { setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); - int degrees = deviceOrientationManager.getMediaOrientation(null); + int degrees = deviceOrientationManager.getVideoOrientation(null); assertEquals(90, degrees); } @Test - public void handleSensorOrientationChange_should_send_message_when_sensor_access_is_allowed() { - try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { - mockedSystem - .when( - () -> - Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) - .thenReturn(1); - setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); - - deviceOrientationManager.handleSensorOrientationChange(90); - } + public void getPhotoOrientation_whenNaturalScreenOrientationEqualsPortraitUp() { + int degreesPortraitUp = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); - verify(mockDartMessenger, times(1)) - .sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); + assertEquals(0, degreesPortraitUp); + assertEquals(90, degreesLandscapeRight); + assertEquals(180, degreesPortraitDown); + assertEquals(270, degreesLandscapeLeft); } @Test - public void - handleSensorOrientationChange_should_send_message_when_sensor_access_is_not_allowed() { - try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { - mockedSystem - .when( - () -> - Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) - .thenReturn(0); - setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + public void getPhotoOrientation_whenNaturalScreenOrientationEqualsLandscapeLeft() { + DeviceOrientationManager orientationManager = + DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 90); - deviceOrientationManager.handleSensorOrientationChange(90); - } + int degreesPortraitUp = orientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + orientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + orientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + orientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); - verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); + assertEquals(90, degreesPortraitUp); + assertEquals(180, degreesLandscapeRight); + assertEquals(270, degreesPortraitDown); + assertEquals(0, degreesLandscapeLeft); } @Test - public void handleUIOrientationChange_should_send_message_when_sensor_access_is_allowed() { - try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { - mockedSystem - .when( - () -> - Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) - .thenReturn(0); - setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + public void getPhotoOrientation_shouldFallbackToCurrentOrientationWhenOrientationIsNull() { + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); - deviceOrientationManager.handleUIOrientationChange(); - } + int degrees = deviceOrientationManager.getPhotoOrientation(null); - verify(mockDartMessenger, times(1)) - .sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); + assertEquals(270, degrees); } @Test - public void handleUIOrientationChange_should_send_message_when_sensor_access_is_not_allowed() { + public void handleUIOrientationChange_shouldSendMessageWhenSensorAccessIsAllowed() { try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { mockedSystem .when( () -> Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) - .thenReturn(1); + .thenReturn(0); setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); deviceOrientationManager.handleUIOrientationChange(); } - verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); + verify(mockDartMessenger, times(1)) + .sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); } @Test - public void handleOrientationChange_should_send_message_when_orientation_is_updated() { + public void handleOrientationChange_shouldSendMessageWhenOrientationIsUpdated() { DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; DeviceOrientation newOrientation = DeviceOrientation.LANDSCAPE_LEFT; - DeviceOrientation orientation = - DeviceOrientationManager.handleOrientationChange( - newOrientation, previousOrientation, mockDartMessenger); + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDartMessenger); verify(mockDartMessenger, times(1)).sendDeviceOrientationChangeEvent(newOrientation); - assertEquals(newOrientation, orientation); } @Test - public void handleOrientationChange_should_not_send_message_when_orientation_is_not_updated() { + public void handleOrientationChange_shouldNotSendMessageWhenOrientationIsNotUpdated() { DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; DeviceOrientation newOrientation = DeviceOrientation.PORTRAIT_UP; - DeviceOrientation orientation = - DeviceOrientationManager.handleOrientationChange( - newOrientation, previousOrientation, mockDartMessenger); + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDartMessenger); verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); - assertEquals(newOrientation, orientation); } @Test diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java index ce2bb7bb2670..2c3a5ab46634 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java @@ -52,7 +52,7 @@ public void after() { } @Test - public void ctor_should_start_device_orientation_manager() { + public void ctor_shouldStartDeviceOrientationManager() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); @@ -60,7 +60,7 @@ public void ctor_should_start_device_orientation_manager() { } @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); @@ -68,7 +68,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { } @Test - public void getValue_should_return_null_if_not_set() { + public void getValue_shouldReturnNullIfNotSet() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); @@ -76,7 +76,7 @@ public void getValue_should_return_null_if_not_set() { } @Test - public void getValue_should_echo_setValue() { + public void getValue_shouldEchoSetValue() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); @@ -86,7 +86,7 @@ public void getValue_should_echo_setValue() { } @Test - public void checkIsSupport_returns_true() { + public void checkIsSupport_returnsTrue() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); @@ -94,8 +94,7 @@ public void checkIsSupport_returns_true() { } @Test - public void - getDeviceOrientationManager_should_return_initialized_DartOrientationManager_instance() { + public void getDeviceOrientationManager_shouldReturnInitializedDartOrientationManagerInstance() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); @@ -104,7 +103,7 @@ public void checkIsSupport_returns_true() { } @Test - public void lockCaptureOrientation_should_lock_to_specified_orientation() { + public void lockCaptureOrientation_shouldLockToSpecifiedOrientation() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); @@ -115,7 +114,7 @@ public void lockCaptureOrientation_should_lock_to_specified_orientation() { } @Test - public void unlockCaptureOrientation_should_set_lock_to_null() { + public void unlockCaptureOrientation_shouldSetLockToNull() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java index c76708a3769e..9f05cc255a8b 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java @@ -50,7 +50,7 @@ public void after() { } @Test - public void ctor_when_parameters_are_valid() { + public void ctor_whenParametersAreValid() { when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(42f); @@ -63,7 +63,7 @@ public void ctor_when_parameters_are_valid() { } @Test - public void ctor_when_sensor_size_is_null() { + public void ctor_whenSensorSizeIsNull() { when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(null); when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(42f); @@ -77,7 +77,7 @@ public void ctor_when_sensor_size_is_null() { } @Test - public void ctor_when_max_zoom_is_null() { + public void ctor_whenMaxZoomIsNull() { when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(null); @@ -91,7 +91,7 @@ public void ctor_when_max_zoom_is_null() { } @Test - public void ctor_when_max_zoom_is_smaller_then_default_zoom_factor() { + public void ctor_whenMaxZoomIsSmallerThenDefaultZoomFactor() { when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(0.5f); @@ -105,21 +105,21 @@ public void ctor_when_max_zoom_is_smaller_then_default_zoom_factor() { } @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); assertEquals("ZoomLevelFeature", zoomLevelFeature.getDebugName()); } @Test - public void getValue_should_return_null_if_not_set() { + public void getValue_shouldReturnNullIfNotSet() { ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); assertEquals(1.0, (float) zoomLevelFeature.getValue(), 0); } @Test - public void getValue_should_echo_setValue() { + public void getValue_shouldEchoSetValue() { ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); zoomLevelFeature.setValue(2.3f); @@ -128,14 +128,14 @@ public void getValue_should_echo_setValue() { } @Test - public void checkIsSupport_returns_false_by_default() { + public void checkIsSupport_returnsFalseByDefault() { ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); assertFalse(zoomLevelFeature.checkIsSupported()); } @Test - public void updateBuilder_should_set_scalar_crop_region_when_checkIsSupport_is_true() { + public void updateBuilder_shouldSetScalarCropRegionWhenCheckIsSupportIsTrue() { when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(42f); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java index f83e5fb11e08..28160ff30714 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java @@ -15,7 +15,7 @@ @RunWith(RobolectricTestRunner.class) public class ZoomUtilsTest { @Test - public void setZoom_when_sensor_size_equals_zero_should_return_crop_region_of_zero() { + public void setZoom_whenSensorSizeEqualsZeroShouldReturnCropRegionOfZero() { final Rect sensorSize = new Rect(0, 0, 0, 0); final Rect computedZoom = ZoomUtils.computeZoom(18f, sensorSize, 1f, 20f); @@ -27,7 +27,7 @@ public void setZoom_when_sensor_size_equals_zero_should_return_crop_region_of_ze } @Test - public void setZoom_when_sensor_size_is_valid_should_return_crop_region() { + public void setZoom_whenSensorSizeIsValidShouldReturnCropRegion() { final Rect sensorSize = new Rect(0, 0, 100, 100); final Rect computedZoom = ZoomUtils.computeZoom(18f, sensorSize, 1f, 20f); @@ -39,7 +39,7 @@ public void setZoom_when_sensor_size_is_valid_should_return_crop_region() { } @Test - public void setZoom_when_zoom_is_greater_then_max_zoom_clamp_to_max_zoom() { + public void setZoom_whenZoomIsGreaterThenMaxZoomClampToMaxZoom() { final Rect sensorSize = new Rect(0, 0, 100, 100); final Rect computedZoom = ZoomUtils.computeZoom(25f, sensorSize, 1f, 10f); @@ -51,7 +51,7 @@ public void setZoom_when_zoom_is_greater_then_max_zoom_clamp_to_max_zoom() { } @Test - public void setZoom_when_zoom_is_smaller_then_min_zoom_clamp_to_min_zoom() { + public void setZoom_whenZoomIsSmallerThenMinZoomClampToMinZoom() { final Rect sensorSize = new Rect(0, 0, 100, 100); final Rect computedZoom = ZoomUtils.computeZoom(0.5f, sensorSize, 1f, 10f); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java index 9b8b54cc959c..5425409c2f3a 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java @@ -24,7 +24,7 @@ public void ctor_test() { } @Test - public void build_Should_set_values_in_correct_order_When_audio_is_disabled() throws IOException { + public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabled() throws IOException { CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); MediaRecorderBuilder.MediaRecorderFactory mockFactory = mock(MediaRecorderBuilder.MediaRecorderFactory.class); @@ -55,7 +55,7 @@ public void build_Should_set_values_in_correct_order_When_audio_is_disabled() th } @Test - public void build_Should_set_values_in_correct_order_When_audio_is_enabled() throws IOException { + public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabled() throws IOException { CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); MediaRecorderBuilder.MediaRecorderFactory mockFactory = mock(MediaRecorderBuilder.MediaRecorderFactory.class); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java index 5f4bd9f89ec7..dbef8510e021 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java @@ -11,7 +11,7 @@ public class ExposureModeTest { @Test - public void getValueForString_returns_correct_values() { + public void getValueForString_returnsCorrectValues() { assertEquals( "Returns ExposureMode.auto for 'auto'", ExposureMode.getValueForString("auto"), @@ -23,13 +23,13 @@ public void getValueForString_returns_correct_values() { } @Test - public void getValueForString_returns_null_for_nonexistant_value() { + public void getValueForString_returnsNullForNonexistantValue() { assertEquals( "Returns null for 'nonexistant'", ExposureMode.getValueForString("nonexistant"), null); } @Test - public void toString_returns_correct_value() { + public void toString_returnsCorrectValue() { assertEquals("Returns 'auto' for ExposureMode.auto", ExposureMode.auto.toString(), "auto"); assertEquals( "Returns 'locked' for ExposureMode.locked", ExposureMode.locked.toString(), "locked"); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java index 5a53648bc51e..7ae175ee4649 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java @@ -11,7 +11,7 @@ public class FlashModeTest { @Test - public void getValueForString_returns_correct_values() { + public void getValueForString_returnsCorrectValues() { assertEquals( "Returns FlashMode.off for 'off'", FlashMode.getValueForString("off"), FlashMode.off); assertEquals( @@ -27,13 +27,13 @@ public void getValueForString_returns_correct_values() { } @Test - public void getValueForString_returns_null_for_nonexistant_value() { + public void getValueForString_returnsNullForNonexistantValue() { assertEquals( "Returns null for 'nonexistant'", FlashMode.getValueForString("nonexistant"), null); } @Test - public void toString_returns_correct_value() { + public void toString_returnsCorrectValue() { assertEquals("Returns 'off' for FlashMode.off", FlashMode.off.toString(), "off"); assertEquals("Returns 'auto' for FlashMode.auto", FlashMode.auto.toString(), "auto"); assertEquals("Returns 'always' for FlashMode.always", FlashMode.always.toString(), "always"); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java index 58e6d7ce3306..1d7b95c1b548 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java @@ -11,7 +11,7 @@ public class FocusModeTest { @Test - public void getValueForString_returns_correct_values() { + public void getValueForString_returnsCorrectValues() { assertEquals( "Returns FocusMode.auto for 'auto'", FocusMode.getValueForString("auto"), FocusMode.auto); assertEquals( @@ -21,13 +21,13 @@ public void getValueForString_returns_correct_values() { } @Test - public void getValueForString_returns_null_for_nonexistant_value() { + public void getValueForString_returnsNullForNonexistantValue() { assertEquals( "Returns null for 'nonexistant'", FocusMode.getValueForString("nonexistant"), null); } @Test - public void toString_returns_correct_value() { + public void toString_returnsCorrectValue() { assertEquals("Returns 'auto' for FocusMode.auto", FocusMode.auto.toString(), "auto"); assertEquals("Returns 'locked' for FocusMode.locked", FocusMode.locked.toString(), "locked"); } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java index 9fc669527bfa..dbf9d11be8b6 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java @@ -23,4 +23,14 @@ public static void setFinalStatic(Class classToModify, String fieldName, Assert.fail("Unable to mock static field: " + fieldName); } } + + public static void setPrivateField(T instance, String fieldName, Object newValue) { + try { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(instance, newValue); + } catch (Exception e) { + Assert.fail("Unable to mock private field: " + fieldName); + } + } } diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 3284a9b01fa2..37869fe78528 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -118,7 +118,7 @@ class CameraValue { /// Whether setting the focus point is supported. final bool focusPointSupported; - /// The current device orientation. + /// The current device UI orientation. final DeviceOrientation deviceOrientation; /// The currently locked capture orientation. diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index ad3175a320a9..1df9f8e2e393 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -61,9 +61,9 @@ class CameraPreview extends StatelessWidget { int _getQuarterTurns() { Map turns = { DeviceOrientation.portraitUp: 0, - DeviceOrientation.landscapeLeft: 1, + DeviceOrientation.landscapeRight: 1, DeviceOrientation.portraitDown: 2, - DeviceOrientation.landscapeRight: 3, + DeviceOrientation.landscapeLeft: 3, }; return turns[_getApplicableOrientation()]!; } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 57161656fc03..a7c6a61a4ef2 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.8.1+7 +version: 0.9.0 environment: sdk: ">=2.12.0 <3.0.0" @@ -25,6 +25,7 @@ dependencies: sdk: flutter pedantic: ^1.10.0 quiver: ^3.0.0 + flutter_plugin_android_lifecycle: ^2.0.2 dev_dependencies: flutter_test: diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index d579341c0e58..8275461192b4 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -146,7 +146,7 @@ void main() { RotatedBox rotatedBox = tester.widget(find.byType(RotatedBox)); - expect(rotatedBox.quarterTurns, 1); + expect(rotatedBox.quarterTurns, 3); debugDefaultTargetPlatformOverride = null; }); @@ -179,7 +179,7 @@ void main() { RotatedBox rotatedBox = tester.widget(find.byType(RotatedBox)); - expect(rotatedBox.quarterTurns, 3); + expect(rotatedBox.quarterTurns, 1); debugDefaultTargetPlatformOverride = null; }); diff --git a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart index c6cedd135fed..ac1c66e4df82 100644 --- a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart +++ b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart @@ -20,8 +20,7 @@ import 'package:flutter/services.dart'; /// They can be (and in fact, are) filtered by the `instanceof`-operator. abstract class DeviceEvent {} -/// The [DeviceOrientationChangedEvent] is fired every time the user changes the -/// physical orientation of the device. +/// The [DeviceOrientationChangedEvent] is fired every time the orientation of the device UI changes. class DeviceOrientationChangedEvent extends DeviceEvent { /// The new orientation of the device final DeviceOrientation orientation; diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index 9e84e8fdf47c..7a7bbf3da592 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -96,11 +96,10 @@ abstract class CameraPlatform extends PlatformInterface { throw UnimplementedError('onCameraTimeLimitReached() is not implemented.'); } - /// The device orientation changed. + /// The ui orientation changed. /// /// Implementations for this: /// - Should support all 4 orientations. - /// - Should not emit new values when the screen orientation is locked. Stream onDeviceOrientationChanged() { throw UnimplementedError( 'onDeviceOrientationChanged() is not implemented.'); From 97f61147c983f7ff4613d9dfecfb0a15d6ff67ed Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 24 Aug 2021 14:42:21 -0400 Subject: [PATCH 087/123] [flutter_plugin_tools] Improve process mocking (#4254) The mock process runner used in most of the tests had poor handling of stdout/stderr: - By default it would return the `List` output from the mock process, which was never correct since the parent process runner interface always sets encodings, thus the `dynamic` should always be of type `String` - The process for setting output on a `MockProcess` was awkward, since it was based on a `Stream>`, even though in every case what we actually want to do is just set the full output to a string. - A hack was at some point added (presumably due to the above issues) to bypass that flow at the process runner level, and instead return a `String` set there. That meant there were two ways of setting output (one of which that only worked for one of the ways of running processes) - That hack wasn't updated when the ability to return multiple mock processes instead of a single global mock process was added, so the API was even more confusing, and there was no way to set different output for different processes. This changes the test APIs so that: - `MockProcess` takes stdout and stderr as strings, and internally manages converting them to a `Stream>`. - `RecordingProcessRunner` correctly decodes and returns the output streams when constructing a process result. It also removes the resultStdout and resultStderr hacks, as well as the legacy `processToReturn` API, and converts all uses to the new structure, which is both simpler to use, and clearly associates output with specific processes. --- script/tool/test/analyze_command_test.dart | 4 +- .../test/build_examples_command_test.dart | 2 +- script/tool/test/common/gradle_test.dart | 2 +- script/tool/test/common/xcode_test.dart | 40 +++++--- .../test/drive_examples_command_test.dart | 10 +- .../test/firebase_test_lab_command_test.dart | 22 ++--- script/tool/test/format_command_test.dart | 30 +++--- .../tool/test/lint_android_command_test.dart | 2 +- .../tool/test/lint_podspecs_command_test.dart | 14 ++- script/tool/test/mocks.dart | 43 +++++--- .../tool/test/native_test_command_test.dart | 60 ++++++++---- .../tool/test/publish_check_command_test.dart | 98 ++++++++----------- .../test/publish_plugin_command_test.dart | 19 ++-- script/tool/test/test_command_test.dart | 10 +- script/tool/test/util.dart | 28 +++--- .../tool/test/xcode_analyze_command_test.dart | 4 +- 16 files changed, 197 insertions(+), 191 deletions(-) diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index da2f0aba86c8..502fa9a0634c 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -211,7 +211,7 @@ void main() { createFakePlugin('foo', packagesDir); processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess.failing() // flutter packages get + MockProcess(exitCode: 1) // flutter packages get ]; Error? commandError; @@ -233,7 +233,7 @@ void main() { createFakePlugin('foo', packagesDir); processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess.failing() // dart analyze + MockProcess(exitCode: 1) // dart analyze ]; Error? commandError; diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 27489a50228a..9c7291c31ddb 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -63,7 +63,7 @@ void main() { processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ - MockProcess.failing() // flutter packages get + MockProcess(exitCode: 1) // flutter packages get ]; Error? commandError; diff --git a/script/tool/test/common/gradle_test.dart b/script/tool/test/common/gradle_test.dart index c24887d3d469..3eac60baf3c3 100644 --- a/script/tool/test/common/gradle_test.dart +++ b/script/tool/test/common/gradle_test.dart @@ -168,7 +168,7 @@ void main() { processRunner.mockProcessesForExecutable[project.gradleWrapper.path] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; final int exitCode = await project.runCommand('foo'); diff --git a/script/tool/test/common/xcode_test.dart b/script/tool/test/common/xcode_test.dart index 7e046a2446c2..259d8ea36cd2 100644 --- a/script/tool/test/common/xcode_test.dart +++ b/script/tool/test/common/xcode_test.dart @@ -94,8 +94,9 @@ void main() { } }; - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = jsonEncode(devices); + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(devices)), + ]; expect(await xcode.findBestAvailableIphoneSimulator(), expectedDeviceId); }); @@ -137,15 +138,16 @@ void main() { } }; - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = jsonEncode(devices); + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(devices)), + ]; expect(await xcode.findBestAvailableIphoneSimulator(), null); }); test('returns null if simctl fails', () async { processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; expect(await xcode.findBestAvailableIphoneSimulator(), null); @@ -216,7 +218,7 @@ void main() { test('returns error codes', () async { processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; final Directory directory = const LocalFileSystem().currentDirectory; @@ -247,8 +249,7 @@ void main() { group('projectHasTarget', () { test('returns true when present', () async { - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = ''' + const String stdout = ''' { "project" : { "configurations" : [ @@ -266,6 +267,9 @@ void main() { ] } }'''; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: stdout), + ]; final Directory project = const LocalFileSystem().directory('/foo.xcodeproj'); @@ -287,8 +291,7 @@ void main() { }); test('returns false when not present', () async { - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = ''' + const String stdout = ''' { "project" : { "configurations" : [ @@ -305,6 +308,9 @@ void main() { ] } }'''; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: stdout), + ]; final Directory project = const LocalFileSystem().directory('/foo.xcodeproj'); @@ -326,8 +332,9 @@ void main() { }); test('returns null for unexpected output', () async { - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = '{}'; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: '{}'), + ]; final Directory project = const LocalFileSystem().directory('/foo.xcodeproj'); @@ -349,8 +356,9 @@ void main() { }); test('returns null for invalid output', () async { - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = ':)'; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: ':)'), + ]; final Directory project = const LocalFileSystem().directory('/foo.xcodeproj'); @@ -372,7 +380,9 @@ void main() { }); test('returns null for failure', () async { - processRunner.processToReturn = MockProcess.failing(); + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(exitCode: 1), // xcodebuild -list + ]; final Directory project = const LocalFileSystem().directory('/foo.xcodeproj'); diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index c6893181e286..bbf865d3edf2 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -60,12 +60,10 @@ void main() { final String output = '''${includeBanner ? updateBanner : ''}[${devices.join(',')}]'''; - final MockProcess mockDevicesProcess = MockProcess.succeeding(); - mockDevicesProcess.stdoutController.close(); // ignore: unawaited_futures + final MockProcess mockDevicesProcess = MockProcess(stdout: output); processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [mockDevicesProcess]; - processRunner.resultStdout = output; } test('fails if no platforms are provided', () async { @@ -151,7 +149,7 @@ void main() { // Simulate failure from `flutter devices`. processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [MockProcess.failing()]; + [MockProcess(exitCode: 1)]; Error? commandError; final List output = await runCapturingPrint( @@ -954,8 +952,8 @@ void main() { .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ // No mock for 'devices', since it's running for macOS. - MockProcess.failing(), // 'drive' #1 - MockProcess.failing(), // 'drive' #2 + MockProcess(exitCode: 1), // 'drive' #1 + MockProcess(exitCode: 1), // 'drive' #2 ]; Error? commandError; diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 35697af3f5fd..7716990b323c 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -40,7 +40,7 @@ void main() { test('fails if gcloud auth fails', () async { processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', @@ -64,8 +64,8 @@ void main() { test('retries gcloud set', () async { processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess.succeeding(), // auth - MockProcess.failing(), // config + MockProcess(), // auth + MockProcess(exitCode: 1), // config ]; createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', @@ -245,10 +245,10 @@ void main() { ]); processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess.succeeding(), // auth - MockProcess.succeeding(), // config - MockProcess.failing(), // integration test #1 - MockProcess.succeeding(), // integration test #2 + MockProcess(), // auth + MockProcess(), // config + MockProcess(exitCode: 1), // integration test #1 + MockProcess(), // integration test #2 ]; Error? commandError; @@ -459,7 +459,7 @@ void main() { ]); processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess.failing() // flutter build + MockProcess(exitCode: 1) // flutter build ]; Error? commandError; @@ -496,7 +496,7 @@ void main() { .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; @@ -533,8 +533,8 @@ void main() { .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.succeeding(), // assembleAndroidTest - MockProcess.failing(), // assembleDebug + MockProcess(), // assembleAndroidTest + MockProcess(exitCode: 1), // assembleDebug ]; Error? commandError; diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index b072e5d30aaf..cf57a9d0dcf7 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -115,7 +115,7 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [MockProcess.failing()]; + [MockProcess(exitCode: 1)]; Error? commandError; final List output = await runCapturingPrint( runner, ['format'], errorHandler: (Error e) { @@ -167,7 +167,7 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable['java'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; final List output = await runCapturingPrint( @@ -193,8 +193,8 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable['java'] = [ - MockProcess.succeeding(), // check for working java - MockProcess.failing(), // format + MockProcess(), // check for working java + MockProcess(exitCode: 1), // format ]; Error? commandError; final List output = await runCapturingPrint( @@ -280,7 +280,7 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable['clang-format'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; final List output = await runCapturingPrint( @@ -335,8 +335,8 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable['clang-format'] = [ - MockProcess.succeeding(), // check for working clang-format - MockProcess.failing(), // format + MockProcess(), // check for working clang-format + MockProcess(exitCode: 1), // format ]; Error? commandError; final List output = await runCapturingPrint( @@ -418,11 +418,11 @@ void main() { ]; createFakePlugin('a_plugin', packagesDir, extraFiles: files); + const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; processRunner.mockProcessesForExecutable['git'] = [ - MockProcess.succeeding(), + MockProcess(stdout: changedFilePath), ]; - const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; - processRunner.resultStdout = changedFilePath; + Error? commandError; final List output = await runCapturingPrint(runner, ['format', '--fail-on-change'], @@ -448,7 +448,7 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable['git'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; final List output = @@ -472,12 +472,12 @@ void main() { ]; createFakePlugin('a_plugin', packagesDir, extraFiles: files); + const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; processRunner.mockProcessesForExecutable['git'] = [ - MockProcess.succeeding(), // ls-files - MockProcess.failing(), // diff + MockProcess(stdout: changedFilePath), // ls-files + MockProcess(exitCode: 1), // diff ]; - const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; - processRunner.resultStdout = changedFilePath; + Error? commandError; final List output = await runCapturingPrint(runner, ['format', '--fail-on-change'], diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart index 05ead220c15b..d08058468636 100644 --- a/script/tool/test/lint_android_command_test.dart +++ b/script/tool/test/lint_android_command_test.dart @@ -101,7 +101,7 @@ void main() { }); processRunner.mockProcessesForExecutable['gradlew'] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; Error? commandError; diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 51a4e6267770..44247274028f 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -75,11 +75,9 @@ void main() { ); processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess.succeeding(), - MockProcess.succeeding(), + MockProcess(stdout: 'Foo', stderr: 'Bar'), + MockProcess(), ]; - processRunner.resultStdout = 'Foo'; - processRunner.resultStderr = 'Bar'; final List output = await runCapturingPrint(runner, ['podspecs']); @@ -173,7 +171,7 @@ void main() { // Simulate failure from `which pod`. processRunner.mockProcessesForExecutable['which'] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; Error? commandError; @@ -199,7 +197,7 @@ void main() { // Simulate failure from `pod`. processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; Error? commandError; @@ -227,8 +225,8 @@ void main() { // Simulate failure from the second call to `pod`. processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess.succeeding(), - MockProcess.failing(), + MockProcess(), + MockProcess(exitCode: 1), ]; Error? commandError; diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart index 0dcdedd3db03..3d0aef1b3971 100644 --- a/script/tool/test/mocks.dart +++ b/script/tool/test/mocks.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io' as io; import 'package:file/file.dart'; @@ -32,22 +33,32 @@ class MockPlatform extends Mock implements Platform { } class MockProcess extends Mock implements io.Process { - MockProcess(); - - /// A mock process that terminates with exitCode 0. - MockProcess.succeeding() { - exitCodeCompleter.complete(0); - } - - /// A mock process that terminates with exitCode 1. - MockProcess.failing() { - exitCodeCompleter.complete(1); + /// Creates a mock process with the given results. + /// + /// The default encodings match the ProcessRunner defaults; mocks for + /// processes run with a different encoding will need to be created with + /// the matching encoding. + MockProcess({ + int exitCode = 0, + String? stdout, + String? stderr, + Encoding stdoutEncoding = io.systemEncoding, + Encoding stderrEncoding = io.systemEncoding, + }) : _exitCode = exitCode { + if (stdout != null) { + _stdoutController.add(stdoutEncoding.encoder.convert(stdout)); + } + if (stderr != null) { + _stderrController.add(stderrEncoding.encoder.convert(stderr)); + } + _stdoutController.close(); + _stderrController.close(); } - final Completer exitCodeCompleter = Completer(); - final StreamController> stdoutController = + final int _exitCode; + final StreamController> _stdoutController = StreamController>(); - final StreamController> stderrController = + final StreamController> _stderrController = StreamController>(); final MockIOSink stdinMock = MockIOSink(); @@ -55,13 +66,13 @@ class MockProcess extends Mock implements io.Process { int get pid => 99; @override - Future get exitCode => exitCodeCompleter.future; + Future get exitCode async => _exitCode; @override - Stream> get stdout => stdoutController.stream; + Stream> get stdout => _stdoutController.stream; @override - Stream> get stderr => stderrController.stream; + Stream> get stderr => _stderrController.stream; @override IOSink get stdin => stdinMock; diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index 59ca17b25c0b..f367dc80182f 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -122,11 +122,9 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - // Exit code 66 from testing indicates no tests. - final MockProcess noTestsProcessResult = MockProcess(); - noTestsProcessResult.exitCodeCompleter.complete(66); processRunner.mockProcessesForExecutable['xcrun'] = [ - noTestsProcessResult, + // Exit code 66 from testing indicates no tests. + MockProcess(exitCode: 66), ]; final List output = await runCapturingPrint(runner, ['native-test', '--macos']); @@ -239,12 +237,13 @@ void main() { 'plugin', packagesDir, platformSupport: { kPlatformIos: PlatformSupport.inline }); - final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = jsonEncode(_kDeviceListMap); + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(_kDeviceListMap)), // simctl + ]; + await runCapturingPrint(runner, ['native-test', '--ios']); expect( @@ -673,7 +672,7 @@ void main() { .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; @@ -745,7 +744,7 @@ void main() { }); processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; @@ -775,9 +774,14 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["RunnerTests", "RunnerUITests"]}}'; + const Map projects = { + 'project': { + 'targets': ['RunnerTests', 'RunnerUITests'] + } + }; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(projects)), // xcodebuild -list + ]; final List output = await runCapturingPrint(runner, [ 'native-test', @@ -835,9 +839,14 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["RunnerTests", "RunnerUITests"]}}'; + const Map projects = { + 'project': { + 'targets': ['RunnerTests', 'RunnerUITests'] + } + }; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(projects)), // xcodebuild -list + ]; final List output = await runCapturingPrint(runner, [ 'native-test', @@ -895,9 +904,16 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); // Simulate a project with unit tests but no integration tests... - processRunner.resultStdout = '{"project":{"targets":["RunnerTests"]}}'; + const Map projects = { + 'project': { + 'targets': ['RunnerTests'] + } + }; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(projects)), // xcodebuild -list + ]; + // ... then try to run only integration tests. final List output = await runCapturingPrint(runner, [ 'native-test', @@ -941,7 +957,9 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.failing(); + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(exitCode: 1), // xcodebuild -list + ]; Error? commandError; final List output = await runCapturingPrint(runner, [ @@ -1192,7 +1210,7 @@ void main() { .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; @@ -1243,11 +1261,11 @@ void main() { .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; // Simulate failing Android. processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index 11de9f095481..65b0cb54547c 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:collection'; import 'dart:convert'; import 'dart:io' as io; @@ -19,18 +18,18 @@ import 'mocks.dart'; import 'util.dart'; void main() { - group('$PublishCheckProcessRunner tests', () { + group('$PublishCheckCommand tests', () { FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; - late PublishCheckProcessRunner processRunner; + late RecordingProcessRunner processRunner; late CommandRunner runner; setUp(() { fileSystem = MemoryFileSystem(); mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = PublishCheckProcessRunner(); + processRunner = RecordingProcessRunner(); final PublishCheckCommand publishCheckCommand = PublishCheckCommand( packagesDir, processRunner: processRunner, @@ -50,12 +49,11 @@ void main() { final Directory plugin2Dir = createFakePlugin('plugin_tools_test_package_b', packagesDir); - processRunner.processesToReturn.add( - MockProcess.succeeding(), - ); - processRunner.processesToReturn.add( - MockProcess.succeeding(), - ); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(), + MockProcess(), + ]; + await runCapturingPrint(runner, ['publish-check']); expect( @@ -75,11 +73,9 @@ void main() { test('fail on negative test', () async { createFakePlugin('plugin_tools_test_package_a', packagesDir); - final MockProcess process = MockProcess.failing(); - process.stdoutController.close(); // ignore: unawaited_futures - process.stderrController.close(); // ignore: unawaited_futures - - processRunner.processesToReturn.add(process); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(exitCode: 1) + ]; expect( () => runCapturingPrint(runner, ['publish-check']), @@ -91,8 +87,9 @@ void main() { final Directory dir = createFakePlugin('c', packagesDir); await dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); - final MockProcess process = MockProcess(); - processRunner.processesToReturn.add(process); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(), + ]; expect(() => runCapturingPrint(runner, ['publish-check']), throwsA(isA())); @@ -101,15 +98,14 @@ void main() { test('pass on prerelease if --allow-pre-release flag is on', () async { createFakePlugin('d', packagesDir); - const String preReleaseOutput = 'Package has 1 warning.' - 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'; - - final MockProcess process = MockProcess.failing(); - process.stdoutController.add(preReleaseOutput.codeUnits); - process.stdoutController.close(); // ignore: unawaited_futures - process.stderrController.close(); // ignore: unawaited_futures - - processRunner.processesToReturn.add(process); + final MockProcess process = MockProcess( + exitCode: 1, + stdout: 'Package has 1 warning.\n' + 'Packages with an SDK constraint on a pre-release of the Dart ' + 'SDK should themselves be published as a pre-release version.'); + processRunner.mockProcessesForExecutable['flutter'] = [ + process, + ]; expect( runCapturingPrint( @@ -120,15 +116,14 @@ void main() { test('fail on prerelease if --allow-pre-release flag is off', () async { createFakePlugin('d', packagesDir); - const String preReleaseOutput = 'Package has 1 warning.' - 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'; - - final MockProcess process = MockProcess.failing(); - process.stdoutController.add(preReleaseOutput.codeUnits); - process.stdoutController.close(); // ignore: unawaited_futures - process.stderrController.close(); // ignore: unawaited_futures - - processRunner.processesToReturn.add(process); + final MockProcess process = MockProcess( + exitCode: 1, + stdout: 'Package has 1 warning.\n' + 'Packages with an SDK constraint on a pre-release of the Dart ' + 'SDK should themselves be published as a pre-release version.'); + processRunner.mockProcessesForExecutable['flutter'] = [ + process, + ]; expect(runCapturingPrint(runner, ['publish-check']), throwsA(isA())); @@ -137,14 +132,9 @@ void main() { test('Success message on stderr is not printed as an error', () async { createFakePlugin('d', packagesDir); - const String publishOutput = 'Package has 0 warnings.'; - - final MockProcess process = MockProcess.succeeding(); - process.stderrController.add(publishOutput.codeUnits); - process.stdoutController.close(); // ignore: unawaited_futures - process.stderrController.close(); // ignore: unawaited_futures - - processRunner.processesToReturn.add(process); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(stdout: 'Package has 0 warnings.'), + ]; final List output = await runCapturingPrint(runner, ['publish-check']); @@ -192,9 +182,6 @@ void main() { createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - processRunner.processesToReturn.add( - MockProcess.succeeding(), - ); final List output = await runCapturingPrint( runner, ['publish-check', '--machine']); @@ -258,9 +245,9 @@ void main() { createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - processRunner.processesToReturn.add( - MockProcess.succeeding(), - ); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(), + ]; final List output = await runCapturingPrint( runner, ['publish-check', '--machine']); @@ -331,9 +318,9 @@ void main() { await plugin1Dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); - processRunner.processesToReturn.add( - MockProcess.succeeding(), - ); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(), + ]; bool hasError = false; final List output = await runCapturingPrint( @@ -369,10 +356,3 @@ void main() { }); }); } - -class PublishCheckProcessRunner extends RecordingProcessRunner { - final Queue processesToReturn = Queue(); - - @override - io.Process get processToReturn => processesToReturn.removeFirst(); -} diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index c7df81952641..9a937daa2384 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -1060,7 +1060,7 @@ class TestProcessRunner extends ProcessRunner { String? mockPublishStdout; String? mockPublishStderr; - int? mockPublishCompleteCode; + int mockPublishCompleteCode = 0; @override Future run( @@ -1097,17 +1097,14 @@ class TestProcessRunner extends ProcessRunner { args[0] == 'pub' && args[1] == 'publish'); mockPublishArgs.addAll(args); - mockPublishProcess = MockProcess(); - if (mockPublishStdout != null) { - mockPublishProcess.stdoutController.add(utf8.encode(mockPublishStdout!)); - } - if (mockPublishStderr != null) { - mockPublishProcess.stderrController.add(utf8.encode(mockPublishStderr!)); - } - if (mockPublishCompleteCode != null) { - mockPublishProcess.exitCodeCompleter.complete(mockPublishCompleteCode); - } + mockPublishProcess = MockProcess( + exitCode: mockPublishCompleteCode, + stdout: mockPublishStdout, + stderr: mockPublishStderr, + stdoutEncoding: utf8, + stderrEncoding: utf8, + ); return mockPublishProcess; } } diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 503e24d03056..3b350f7d88ae 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -67,8 +67,8 @@ void main() { processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ - MockProcess.failing(), // plugin 1 test - MockProcess.succeeding(), // plugin 2 test + MockProcess(exitCode: 1), // plugin 1 test + MockProcess(), // plugin 2 test ]; Error? commandError; @@ -132,7 +132,7 @@ void main() { extraFiles: ['test/empty_test.dart']); processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess.failing(), // dart pub get + MockProcess(exitCode: 1), // dart pub get ]; Error? commandError; @@ -156,8 +156,8 @@ void main() { extraFiles: ['test/empty_test.dart']); processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess.succeeding(), // dart pub get - MockProcess.failing(), // dart pub run test + MockProcess(), // dart pub get + MockProcess(exitCode: 1), // dart pub run test ]; Error? commandError; diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 1984a25cc430..10a85f49e815 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -265,15 +265,6 @@ class RecordingProcessRunner extends ProcessRunner { final Map> mockProcessesForExecutable = >{}; - /// Populate for [io.ProcessResult] to use a String [stdout] instead of a [List] of [int]. - String? resultStdout; - - /// Populate for [io.ProcessResult] to use a String [stderr] instead of a [List] of [int]. - String? resultStderr; - - // Deprecated--do not add new uses. Use mockProcessesForExecutable instead. - io.Process? processToReturn; - @override Future runAndStream( String executable, @@ -291,8 +282,7 @@ class RecordingProcessRunner extends ProcessRunner { return Future.value(exitCode); } - /// Returns [io.ProcessResult] created from [mockProcessesForExecutable], - /// [resultStdout], and [resultStderr]. + /// Returns [io.ProcessResult] created from [mockProcessesForExecutable]. @override Future run( String executable, @@ -306,10 +296,16 @@ class RecordingProcessRunner extends ProcessRunner { recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); final io.Process? process = _getProcessToReturn(executable); + final List? processStdout = + await process?.stdout.transform(stdoutEncoding.decoder).toList(); + final String stdout = processStdout?.join('') ?? ''; + final List? processStderr = + await process?.stderr.transform(stderrEncoding.decoder).toList(); + final String stderr = processStderr?.join('') ?? ''; + final io.ProcessResult result = process == null ? io.ProcessResult(1, 0, '', '') - : io.ProcessResult(process.pid, await process.exitCode, - resultStdout ?? process.stdout, resultStderr ?? process.stderr); + : io.ProcessResult(process.pid, await process.exitCode, stdout, stderr); if (exitOnError && (result.exitCode != 0)) { throw io.ProcessException(executable, args); @@ -326,13 +322,11 @@ class RecordingProcessRunner extends ProcessRunner { } io.Process? _getProcessToReturn(String executable) { - io.Process? process; final List? processes = mockProcessesForExecutable[executable]; if (processes != null && processes.isNotEmpty) { - process = mockProcessesForExecutable[executable]!.removeAt(0); + return processes.removeAt(0); } - // Fall back to `processToReturn` for backwards compatibility. - return process ?? processToReturn; + return null; } } diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart index b715ac531f50..790a526a8ae0 100644 --- a/script/tool/test/xcode_analyze_command_test.dart +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -131,7 +131,7 @@ void main() { }); processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; @@ -228,7 +228,7 @@ void main() { }); processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; From fb6622092bb8f1a56b701f6c6ede551b6c986d06 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 24 Aug 2021 16:29:56 -0400 Subject: [PATCH 088/123] [flutter_plugin_tools] Introduce a class for packages (#4252) Packages are the primary conceptual object in the tool, but currently they are represented simply as Directory (or occasionally a path string). This introduces an object for packages and: - moves a number of existing utility methods into it - sweeps the code for the obvious cases of using `Directory` to represent a package, especially in method signatures and migrates them - notes a few places where we should migrate later, to avoid ballooning the size of the PR There are no doubt other cases not caught in the sweep, but this gives us a foundation both for new code, and to migrate incrementally toward as we find existing code that was missed. --- script/tool/lib/src/analyze_command.dart | 14 +- .../tool/lib/src/build_examples_command.dart | 11 +- .../src/common/package_looping_command.dart | 86 +++++------- .../tool/lib/src/common/plugin_command.dart | 54 +++----- script/tool/lib/src/common/plugin_utils.dart | 42 +----- .../lib/src/common/pub_version_finder.dart | 6 +- .../lib/src/common/repository_package.dart | 78 +++++++++++ .../src/create_all_plugins_app_command.dart | 6 +- .../tool/lib/src/drive_examples_command.dart | 34 ++--- .../lib/src/firebase_test_lab_command.dart | 26 ++-- script/tool/lib/src/lint_android_command.dart | 9 +- .../tool/lib/src/lint_podspecs_command.dart | 5 +- script/tool/lib/src/list_command.dart | 18 +-- script/tool/lib/src/native_test_command.dart | 50 +++---- .../tool/lib/src/publish_check_command.dart | 18 +-- .../tool/lib/src/publish_plugin_command.dart | 14 +- .../tool/lib/src/pubspec_check_command.dart | 19 +-- script/tool/lib/src/test_command.dart | 19 +-- .../tool/lib/src/version_check_command.dart | 19 +-- .../tool/lib/src/xcode_analyze_command.dart | 13 +- .../common/package_looping_command_test.dart | 67 +--------- .../tool/test/common/plugin_command_test.dart | 10 +- .../tool/test/common/plugin_utils_test.dart | 21 +-- .../test/common/pub_version_finder_test.dart | 6 +- .../test/common/repository_package_test.dart | 123 ++++++++++++++++++ script/tool/test/format_command_test.dart | 4 +- script/tool/test/util.dart | 2 + 27 files changed, 440 insertions(+), 334 deletions(-) create mode 100644 script/tool/lib/src/common/repository_package.dart create mode 100644 script/tool/test/common/repository_package_test.dart diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 2b728e2b9073..faad7f4736eb 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -5,13 +5,14 @@ import 'dart:async'; import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_command.dart'; import 'package:platform/platform.dart'; import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; +import 'common/plugin_command.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; const int _exitPackagesGetFailed = 3; @@ -55,8 +56,9 @@ class AnalyzeCommand extends PackageLoopingCommand { final bool hasLongOutput = false; /// Checks that there are no unexpected analysis_options.yaml files. - bool _hasUnexpecetdAnalysisOptions(Directory package) { - final List files = package.listSync(recursive: true); + bool _hasUnexpecetdAnalysisOptions(RepositoryPackage package) { + final List files = + package.directory.listSync(recursive: true); for (final FileSystemEntity file in files) { if (file.basename != 'analysis_options.yaml' && file.basename != '.analysis_options') { @@ -87,7 +89,7 @@ class AnalyzeCommand extends PackageLoopingCommand { Future _runPackagesGetOnTargetPackages() async { final List packageDirectories = await getTargetPackagesAndSubpackages() - .map((PackageEnumerationEntry package) => package.directory) + .map((PackageEnumerationEntry entry) => entry.package.directory) .toList(); final Set packagePaths = packageDirectories.map((Directory dir) => dir.path).toSet(); @@ -135,13 +137,13 @@ class AnalyzeCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { if (_hasUnexpecetdAnalysisOptions(package)) { return PackageResult.fail(['Unexpected local analysis options']); } final int exitCode = await processRunner.runAndStream( _dartBinaryPath, ['analyze', '--fatal-infos'], - workingDir: package); + workingDir: package.directory); if (exitCode != 0) { return PackageResult.fail(); } diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 0cac09980c94..ac5e84b7c3c7 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -11,6 +11,7 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; /// Key for APK. const String _platformFlagApk = 'apk'; @@ -96,7 +97,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final List errors = []; final Iterable<_PlatformDetails> requestedPlatforms = _platforms.entries @@ -126,9 +127,9 @@ class BuildExamplesCommand extends PackageLoopingCommand { } print(''); - for (final Directory example in getExamplesForPlugin(package)) { + for (final RepositoryPackage example in package.getExamples()) { final String packageName = - getRelativePosixPath(example, from: packagesDir); + getRelativePosixPath(example.directory, from: packagesDir); for (final _PlatformDetails platform in buildPlatforms) { String buildPlatform = platform.label; @@ -149,7 +150,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { } Future _buildExample( - Directory example, + RepositoryPackage example, String flutterBuildType, { List extraBuildFlags = const [], }) async { @@ -164,7 +165,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', ], - workingDir: example, + workingDir: example.directory, ); return exitCode == 0; } diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 0e0976ecc6a7..00caeb30ef42 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -13,6 +13,7 @@ import 'package:platform/platform.dart'; import 'core.dart'; import 'plugin_command.dart'; import 'process_runner.dart'; +import 'repository_package.dart'; /// Possible outcomes of a command run for a package. enum RunState { @@ -84,7 +85,7 @@ abstract class PackageLoopingCommand extends PluginCommand { int _otherWarningCount = 0; /// The package currently being run by [runForPackage]. - PackageEnumerationEntry? _currentPackage; + PackageEnumerationEntry? _currentPackageEntry; /// Called during [run] before any calls to [runForPackage]. This provides an /// opportunity to fail early if the command can't be run (e.g., because the @@ -97,7 +98,7 @@ abstract class PackageLoopingCommand extends PluginCommand { /// be included in the final error summary (e.g., a command that only has a /// single failure mode), or strings that should be listed for that package /// in the final summary. An empty list indicates success. - Future runForPackage(Directory package); + Future runForPackage(RepositoryPackage package); /// Called during [run] after all calls to [runForPackage]. This provides an /// opportunity to do any cleanup of run-level state. @@ -155,31 +156,13 @@ abstract class PackageLoopingCommand extends PluginCommand { /// things that might be useful to someone debugging an unexpected result. void logWarning(String warningMessage) { print(Colorize(warningMessage)..yellow()); - if (_currentPackage != null) { - _packagesWithWarnings.add(_currentPackage!); + if (_currentPackageEntry != null) { + _packagesWithWarnings.add(_currentPackageEntry!); } else { ++_otherWarningCount; } } - /// Returns the identifying name to use for [package]. - /// - /// Implementations should not expect a specific format for this string, since - /// it uses heuristics to try to be precise without being overly verbose. If - /// an exact format (e.g., published name, or basename) is required, that - /// should be used instead. - String getPackageDescription(Directory package) { - String packageName = getRelativePosixPath(package, from: packagesDir); - final List components = p.posix.split(packageName); - // For the common federated plugin pattern of `foo/foo_subpackage`, drop - // the first part since it's not useful. - if (components.length >= 2 && - components[1].startsWith('${components[0]}_')) { - packageName = p.posix.joinAll(components.sublist(1)); - } - return packageName; - } - /// Returns the relative path from [from] to [entity] in Posix style. /// /// This should be used when, for example, printing package-relative paths in @@ -219,36 +202,36 @@ abstract class PackageLoopingCommand extends PluginCommand { Future _runInternal() async { _packagesWithWarnings.clear(); _otherWarningCount = 0; - _currentPackage = null; + _currentPackageEntry = null; await initializeRun(); - final List packages = includeSubpackages + final List targetPackages = includeSubpackages ? await getTargetPackagesAndSubpackages(filterExcluded: false).toList() : await getTargetPackages(filterExcluded: false).toList(); final Map results = {}; - for (final PackageEnumerationEntry package in packages) { - _currentPackage = package; - _printPackageHeading(package); + for (final PackageEnumerationEntry entry in targetPackages) { + _currentPackageEntry = entry; + _printPackageHeading(entry); // Command implementations should never see excluded packages; they are // included at this level only for logging. - if (package.excluded) { - results[package] = PackageResult.exclude(); + if (entry.excluded) { + results[entry] = PackageResult.exclude(); continue; } - final PackageResult result = await runForPackage(package.directory); + final PackageResult result = await runForPackage(entry.package); if (result.state == RunState.skipped) { final String message = '${indentation}SKIPPING: ${result.details.first}'; captureOutput ? print(message) : print(Colorize(message)..darkGray()); } - results[package] = result; + results[entry] = result; } - _currentPackage = null; + _currentPackageEntry = null; completeRun(); @@ -256,13 +239,13 @@ abstract class PackageLoopingCommand extends PluginCommand { // If there were any errors reported, summarize them and exit. if (results.values .any((PackageResult result) => result.state == RunState.failed)) { - _printFailureSummary(packages, results); + _printFailureSummary(targetPackages, results); return false; } // Otherwise, print a summary of what ran for ease of auditing that all the // expected tests ran. - _printRunSummary(packages, results); + _printRunSummary(targetPackages, results); print('\n'); _printSuccess('No issues found!'); @@ -283,9 +266,9 @@ abstract class PackageLoopingCommand extends PluginCommand { /// Something is always printed to make it easier to distinguish between /// a command running for a package and producing no output, and a command /// not having been run for a package. - void _printPackageHeading(PackageEnumerationEntry package) { - final String packageDisplayName = getPackageDescription(package.directory); - String heading = package.excluded + void _printPackageHeading(PackageEnumerationEntry entry) { + final String packageDisplayName = entry.package.displayName; + String heading = entry.excluded ? 'Not running for $packageDisplayName; excluded' : 'Running for $packageDisplayName'; if (hasLongOutput) { @@ -295,16 +278,15 @@ abstract class PackageLoopingCommand extends PluginCommand { || $heading ============================================================ '''; - } else if (!package.excluded) { + } else if (!entry.excluded) { heading = '$heading...'; } if (captureOutput) { print(heading); } else { final Colorize colorizeHeading = Colorize(heading); - print(package.excluded - ? colorizeHeading.darkGray() - : colorizeHeading.cyan()); + print( + entry.excluded ? colorizeHeading.darkGray() : colorizeHeading.cyan()); } } @@ -349,17 +331,18 @@ abstract class PackageLoopingCommand extends PluginCommand { /// Prints a one-line-per-package overview of the run results for each /// package. - void _printPerPackageRunOverview(List packages, + void _printPerPackageRunOverview( + List packageEnumeration, {required Set skipped}) { print('Run overview:'); - for (final PackageEnumerationEntry package in packages) { - final bool hadWarning = _packagesWithWarnings.contains(package); + for (final PackageEnumerationEntry entry in packageEnumeration) { + final bool hadWarning = _packagesWithWarnings.contains(entry); Styles style; String summary; - if (package.excluded) { + if (entry.excluded) { summary = 'excluded'; style = Styles.DARK_GRAY; - } else if (skipped.contains(package)) { + } else if (skipped.contains(entry)) { summary = 'skipped'; style = hadWarning ? Styles.LIGHT_YELLOW : Styles.DARK_GRAY; } else { @@ -373,18 +356,18 @@ abstract class PackageLoopingCommand extends PluginCommand { if (!captureOutput) { summary = (Colorize(summary)..apply(style)).toString(); } - print(' ${getPackageDescription(package.directory)} - $summary'); + print(' ${entry.package.displayName} - $summary'); } print(''); } /// Prints a summary of all of the failures from [results]. - void _printFailureSummary(List packages, + void _printFailureSummary(List packageEnumeration, Map results) { const String indentation = ' '; _printError(failureListHeader); - for (final PackageEnumerationEntry package in packages) { - final PackageResult result = results[package]!; + for (final PackageEnumerationEntry entry in packageEnumeration) { + final PackageResult result = results[entry]!; if (result.state == RunState.failed) { final String errorIndentation = indentation * 2; String errorDetails = ''; @@ -392,8 +375,7 @@ abstract class PackageLoopingCommand extends PluginCommand { errorDetails = ':\n$errorIndentation${result.details.join('\n$errorIndentation')}'; } - _printError( - '$indentation${getPackageDescription(package.directory)}$errorDetails'); + _printError('$indentation${entry.package.displayName}$errorDetails'); } } _printError(failureListFooter); diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 10f423360878..ec51261ab617 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -14,15 +14,18 @@ import 'package:yaml/yaml.dart'; import 'core.dart'; import 'git_version_finder.dart'; import 'process_runner.dart'; +import 'repository_package.dart'; /// An entry in package enumeration for APIs that need to include extra /// data about the entry. class PackageEnumerationEntry { - /// Creates a new entry for the given package directory. - PackageEnumerationEntry(this.directory, {required this.excluded}); + /// Creates a new entry for the given package. + PackageEnumerationEntry(this.package, {required this.excluded}); - /// The package's location. - final Directory directory; + /// The package this entry corresponds to. Be sure to check `excluded` before + /// using this, as having an entry does not necessarily mean that the package + /// should be included in the processing of the enumeration. + final RepositoryPackage package; /// Whether or not this package was excluded by the command invocation. final bool excluded; @@ -225,7 +228,7 @@ abstract class PluginCommand extends Command { final List allPlugins = await _getAllPackages().toList(); allPlugins.sort((PackageEnumerationEntry p1, PackageEnumerationEntry p2) => - p1.directory.path.compareTo(p2.directory.path)); + p1.package.path.compareTo(p2.package.path)); final int shardSize = allPlugins.length ~/ shardCount + (allPlugins.length % shardCount == 0 ? 0 : 1); final int start = min(shardIndex * shardSize, allPlugins.length); @@ -287,7 +290,8 @@ abstract class PluginCommand extends Command { // A top-level Dart package is a plugin package. if (_isDartPackage(entity)) { if (plugins.isEmpty || plugins.contains(p.basename(entity.path))) { - yield PackageEnumerationEntry(entity as Directory, + yield PackageEnumerationEntry( + RepositoryPackage(entity as Directory), excluded: excludedPluginNames.contains(entity.basename)); } } else if (entity is Directory) { @@ -305,7 +309,8 @@ abstract class PluginCommand extends Command { if (plugins.isEmpty || plugins.contains(relativePath) || plugins.contains(basenamePath)) { - yield PackageEnumerationEntry(subdir as Directory, + yield PackageEnumerationEntry( + RepositoryPackage(subdir as Directory), excluded: excludedPluginNames.contains(basenamePath) || excludedPluginNames.contains(packageName) || excludedPluginNames.contains(relativePath)); @@ -327,26 +332,26 @@ abstract class PluginCommand extends Command { await for (final PackageEnumerationEntry plugin in getTargetPackages(filterExcluded: filterExcluded)) { yield plugin; - yield* plugin.directory + yield* plugin.package.directory .list(recursive: true, followLinks: false) .where(_isDartPackage) .map((FileSystemEntity directory) => PackageEnumerationEntry( - directory as Directory, // _isDartPackage guarantees this works. + // _isDartPackage guarantees that this cast is valid. + RepositoryPackage(directory as Directory), excluded: plugin.excluded)); } } - /// Returns the files contained, recursively, within the plugins + /// Returns the files contained, recursively, within the packages /// involved in this command execution. Stream getFiles() { - return getTargetPackages() - .map((PackageEnumerationEntry entry) => entry.directory) - .asyncExpand((Directory folder) => getFilesForPackage(folder)); + return getTargetPackages().asyncExpand( + (PackageEnumerationEntry entry) => getFilesForPackage(entry.package)); } /// Returns the files contained, recursively, within [package]. - Stream getFilesForPackage(Directory package) { - return package + Stream getFilesForPackage(RepositoryPackage package) { + return package.directory .list(recursive: true, followLinks: false) .where((FileSystemEntity entity) => entity is File) .cast(); @@ -358,25 +363,6 @@ abstract class PluginCommand extends Command { return entity is Directory && entity.childFile('pubspec.yaml').existsSync(); } - /// Returns the example Dart packages contained in the specified plugin, or - /// an empty List, if the plugin has no examples. - Iterable getExamplesForPlugin(Directory plugin) { - final Directory exampleFolder = plugin.childDirectory('example'); - if (!exampleFolder.existsSync()) { - return []; - } - if (isFlutterPackage(exampleFolder)) { - return [exampleFolder]; - } - // Only look at the subdirectories of the example directory if the example - // directory itself is not a Dart package, and only look one level below the - // example directory for other dart packages. - return exampleFolder - .listSync() - .where((FileSystemEntity entity) => isFlutterPackage(entity)) - .cast(); - } - /// Retrieve an instance of [GitVersionFinder] based on `_kBaseSha` and [gitDir]. /// /// Throws tool exit if [gitDir] nor root directory is a git directory. diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index 0277b78d566a..d9c42e220c0b 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:yaml/yaml.dart'; import 'core.dart'; @@ -16,7 +17,7 @@ enum PlatformSupport { federated, } -/// Returns whether the given directory contains a Flutter [platform] plugin. +/// Returns whether the given [package] is a Flutter [platform] plugin. /// /// It checks this by looking for the following pattern in the pubspec: /// @@ -27,7 +28,7 @@ enum PlatformSupport { /// /// If [requiredMode] is provided, the plugin must have the given type of /// implementation in order to return true. -bool pluginSupportsPlatform(String platform, FileSystemEntity entity, +bool pluginSupportsPlatform(String platform, RepositoryPackage package, {PlatformSupport? requiredMode}) { assert(platform == kPlatformIos || platform == kPlatformAndroid || @@ -35,14 +36,9 @@ bool pluginSupportsPlatform(String platform, FileSystemEntity entity, platform == kPlatformMacos || platform == kPlatformWindows || platform == kPlatformLinux); - if (entity is! Directory) { - return false; - } - try { - final File pubspecFile = entity.childFile('pubspec.yaml'); final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + loadYaml(package.pubspecFile.readAsStringSync()) as YamlMap; final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; if (flutterSection == null) { return false; @@ -78,33 +74,3 @@ bool pluginSupportsPlatform(String platform, FileSystemEntity entity, return false; } } - -/// Returns whether the given directory contains a Flutter Android plugin. -bool isAndroidPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformAndroid, entity); -} - -/// Returns whether the given directory contains a Flutter iOS plugin. -bool isIosPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformIos, entity); -} - -/// Returns whether the given directory contains a Flutter web plugin. -bool isWebPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformWeb, entity); -} - -/// Returns whether the given directory contains a Flutter Windows plugin. -bool isWindowsPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformWindows, entity); -} - -/// Returns whether the given directory contains a Flutter macOS plugin. -bool isMacOsPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformMacos, entity); -} - -/// Returns whether the given directory contains a Flutter linux plugin. -bool isLinuxPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformLinux, entity); -} diff --git a/script/tool/lib/src/common/pub_version_finder.dart b/script/tool/lib/src/common/pub_version_finder.dart index ebac473de7ac..572cb913aa7d 100644 --- a/script/tool/lib/src/common/pub_version_finder.dart +++ b/script/tool/lib/src/common/pub_version_finder.dart @@ -27,10 +27,10 @@ class PubVersionFinder { /// Get the package version on pub. Future getPackageVersion( - {required String package}) async { - assert(package.isNotEmpty); + {required String packageName}) async { + assert(packageName.isNotEmpty); final Uri pubHostUri = Uri.parse(pubHost); - final Uri url = pubHostUri.replace(path: '/packages/$package.json'); + final Uri url = pubHostUri.replace(path: '/packages/$packageName.json'); final http.Response response = await httpClient.get(url); if (response.statusCode == 404) { diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart new file mode 100644 index 000000000000..f6601d39b79e --- /dev/null +++ b/script/tool/lib/src/common/repository_package.dart @@ -0,0 +1,78 @@ +// 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:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'core.dart'; + +/// A package in the repository. +// +// TODO(stuartmorgan): Add more package-related info here, such as an on-demand +// cache of the parsed pubspec. +class RepositoryPackage { + /// Creates a representation of the package at [directory]. + RepositoryPackage(this.directory); + + /// The location of the package. + final Directory directory; + + /// The path to the package. + String get path => directory.path; + + /// Returns the string to use when referring to the package in user-targeted + /// messages. + /// + /// Callers should not expect a specific format for this string, since + /// it uses heuristics to try to be precise without being overly verbose. If + /// an exact format (e.g., published name, or basename) is required, that + /// should be used instead. + String get displayName { + List components = directory.fileSystem.path.split(directory.path); + // Remove everything up to the packages directory. + final int packagesIndex = components.indexOf('packages'); + if (packagesIndex != -1) { + components = components.sublist(packagesIndex + 1); + } + // For the common federated plugin pattern of `foo/foo_subpackage`, drop + // the first part since it's not useful. + if (components.length >= 2 && + components[1].startsWith('${components[0]}_')) { + components = components.sublist(1); + } + return p.posix.joinAll(components); + } + + /// The package's top-level pubspec.yaml. + File get pubspecFile => directory.childFile('pubspec.yaml'); + + /// Returns the Flutter example packages contained in the package, if any. + Iterable getExamples() { + final Directory exampleDirectory = directory.childDirectory('example'); + if (!exampleDirectory.existsSync()) { + return []; + } + if (isFlutterPackage(exampleDirectory)) { + return [RepositoryPackage(exampleDirectory)]; + } + // Only look at the subdirectories of the example directory if the example + // directory itself is not a Dart package, and only look one level below the + // example directory for other Dart packages. + return exampleDirectory + .listSync() + .where((FileSystemEntity entity) => isFlutterPackage(entity)) + // isFlutterPackage guarantees that the cast to Directory is safe. + .map((FileSystemEntity entity) => + RepositoryPackage(entity as Directory)); + } + + /// Returns the example directory, assuming there is only one. + /// + /// DO NOT USE THIS METHOD. It exists only to easily find code that was + /// written to use a single example and needs to be restructured to handle + /// multiple examples. New code should always use [getExamples]. + // TODO(stuartmorgan): Eliminate all uses of this. + RepositoryPackage getSingleExampleDeprecated() => + RepositoryPackage(directory.childDirectory('example')); +} diff --git a/script/tool/lib/src/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_plugins_app_command.dart index e1cee6f3fe7d..6dbebf2f5c74 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -11,6 +11,7 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/plugin_command.dart'; +import 'common/repository_package.dart'; const String _outputDirectoryFlag = 'output-dir'; @@ -170,10 +171,11 @@ class CreateAllPluginsAppCommand extends PluginCommand { final Map pathDependencies = {}; - await for (final PackageEnumerationEntry package in getTargetPackages()) { + await for (final PackageEnumerationEntry entry in getTargetPackages()) { + final RepositoryPackage package = entry.package; final Directory pluginDirectory = package.directory; final String pluginName = pluginDirectory.basename; - final File pubspecFile = pluginDirectory.childFile('pubspec.yaml'); + final File pubspecFile = package.pubspecFile; final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); if (pubspec.publishTo != 'none') { diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 7e800ed54866..3605dcce1f22 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -12,6 +12,7 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; const int _exitNoPlatformFlags = 2; const int _exitNoAvailableDevice = 3; @@ -119,9 +120,9 @@ class DriveExamplesCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { - if (package.basename.endsWith('_platform_interface') && - !package.childDirectory('example').existsSync()) { + Future runForPackage(RepositoryPackage package) async { + if (package.directory.basename.endsWith('_platform_interface') && + !package.getSingleExampleDeprecated().directory.existsSync()) { // Platform interface packages generally aren't intended to have // examples, and don't need integration tests, so skip rather than fail. return PackageResult.skip( @@ -140,16 +141,16 @@ class DriveExamplesCommand extends PackageLoopingCommand { // If there is no supported target platform, skip the plugin. if (deviceFlags.isEmpty) { return PackageResult.skip( - '${getPackageDescription(package)} does not support any requested platform.'); + '${package.displayName} does not support any requested platform.'); } int examplesFound = 0; bool testsRan = false; final List errors = []; - for (final Directory example in getExamplesForPlugin(package)) { + for (final RepositoryPackage example in package.getExamples()) { ++examplesFound; final String exampleName = - getRelativePosixPath(example, from: packagesDir); + getRelativePosixPath(example.directory, from: packagesDir); final List drivers = await _getDrivers(example); if (drivers.isEmpty) { @@ -173,7 +174,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { if (testTargets.isEmpty) { final String driverRelativePath = - getRelativePosixPath(driver, from: package); + getRelativePosixPath(driver, from: package.directory); printError( 'Found $driverRelativePath, but no integration_test/*_test.dart files.'); errors.add('No test files for $driverRelativePath'); @@ -185,7 +186,8 @@ class DriveExamplesCommand extends PackageLoopingCommand { example, driver, testTargets, deviceFlags: deviceFlags); for (final File failingTarget in failingTargets) { - errors.add(getRelativePosixPath(failingTarget, from: package)); + errors.add( + getRelativePosixPath(failingTarget, from: package.directory)); } } } @@ -229,10 +231,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { return deviceIds; } - Future> _getDrivers(Directory example) async { + Future> _getDrivers(RepositoryPackage example) async { final List drivers = []; - final Directory driverDir = example.childDirectory('test_driver'); + final Directory driverDir = example.directory.childDirectory('test_driver'); if (driverDir.existsSync()) { await for (final FileSystemEntity driver in driverDir.list()) { if (driver is File && driver.basename.endsWith('_test.dart')) { @@ -253,10 +255,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { return testFile.existsSync() ? testFile : null; } - Future> _getIntegrationTests(Directory example) async { + Future> _getIntegrationTests(RepositoryPackage example) async { final List tests = []; final Directory integrationTestDir = - example.childDirectory('integration_test'); + example.directory.childDirectory('integration_test'); if (integrationTestDir.existsSync()) { await for (final FileSystemEntity file in integrationTestDir.list()) { @@ -278,7 +280,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { /// - `['-d', 'web-server', '--web-port=', '--browser-name=]` /// for web Future> _driveTests( - Directory example, + RepositoryPackage example, File driver, List targets, { required List deviceFlags, @@ -296,11 +298,11 @@ class DriveExamplesCommand extends PackageLoopingCommand { if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', '--driver', - getRelativePosixPath(driver, from: example), + getRelativePosixPath(driver, from: example.directory), '--target', - getRelativePosixPath(target, from: example), + getRelativePosixPath(target, from: example.directory), ], - workingDir: example); + workingDir: example.directory); if (exitCode != 0) { failures.add(target); } diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index fd2de97be4b3..4fc47c0da70c 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -13,6 +13,7 @@ import 'common/core.dart'; import 'common/gradle.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; const int _exitGcloudAuthFailed = 2; @@ -117,13 +118,13 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { - final Directory exampleDirectory = package.childDirectory('example'); + Future runForPackage(RepositoryPackage package) async { + final RepositoryPackage example = package.getSingleExampleDeprecated(); final Directory androidDirectory = - exampleDirectory.childDirectory('android'); + example.directory.childDirectory('android'); if (!androidDirectory.existsSync()) { return PackageResult.skip( - '${getPackageDescription(exampleDirectory)} does not support Android.'); + '${example.displayName} does not support Android.'); } if (!androidDirectory @@ -137,7 +138,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { } // Ensures that gradle wrapper exists - final GradleProject project = GradleProject(exampleDirectory, + final GradleProject project = GradleProject(example.directory, processRunner: processRunner, platform: platform); if (!await _ensureGradleWrapperExists(project)) { return PackageResult.fail(['Unable to build example apk']); @@ -155,7 +156,8 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { // test file's run. int resultsCounter = 0; for (final File test in _findIntegrationTestFiles(package)) { - final String testName = getRelativePosixPath(test, from: package); + final String testName = + getRelativePosixPath(test, from: package.directory); print('Testing $testName...'); if (!await _runGradle(project, 'app:assembleDebug', testFile: test)) { printError('Could not build $testName'); @@ -165,7 +167,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { final String buildId = getStringArg('build-id'); final String testRunId = getStringArg('test-run-id'); final String resultsDir = - 'plugins_android_test/${getPackageDescription(package)}/$buildId/$testRunId/${resultsCounter++}/'; + 'plugins_android_test/${package.displayName}/$buildId/$testRunId/${resultsCounter++}/'; final List args = [ 'firebase', 'test', @@ -186,7 +188,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { args.addAll(['--device', device]); } final int exitCode = await processRunner.runAndStream('gcloud', args, - workingDir: exampleDirectory); + workingDir: example.directory); if (exitCode != 0) { printError('Test failure for $testName'); @@ -262,9 +264,11 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { } /// Finds and returns all integration test files for [package]. - Iterable _findIntegrationTestFiles(Directory package) sync* { - final Directory integrationTestDir = - package.childDirectory('example').childDirectory('integration_test'); + Iterable _findIntegrationTestFiles(RepositoryPackage package) sync* { + final Directory integrationTestDir = package + .getSingleExampleDeprecated() + .directory + .childDirectory('integration_test'); if (!integrationTestDir.existsSync()) { return; diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart index be6c6ed32415..a7b5c4f2e8bf 100644 --- a/script/tool/lib/src/lint_android_command.dart +++ b/script/tool/lib/src/lint_android_command.dart @@ -10,6 +10,7 @@ import 'common/core.dart'; import 'common/gradle.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; /// Lint the CocoaPod podspecs and run unit tests. /// @@ -30,22 +31,22 @@ class LintAndroidCommand extends PackageLoopingCommand { 'Requires the example to have been build at least once before running.'; @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { if (!pluginSupportsPlatform(kPlatformAndroid, package, requiredMode: PlatformSupport.inline)) { return PackageResult.skip( 'Plugin does not have an Android implemenatation.'); } - final Directory exampleDirectory = package.childDirectory('example'); - final GradleProject project = GradleProject(exampleDirectory, + final RepositoryPackage example = package.getSingleExampleDeprecated(); + final GradleProject project = GradleProject(example.directory, processRunner: processRunner, platform: platform); if (!project.isConfigured()) { return PackageResult.fail(['Build example before linting']); } - final String packageName = package.basename; + final String packageName = package.directory.basename; // Only lint one build mode to avoid extra work. // Only lint the plugin project itself, to avoid failing due to errors in diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index d0d93fcb79b1..ee44a82da5b9 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -12,6 +12,7 @@ import 'package:platform/platform.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; const int _exitUnsupportedPlatform = 2; const int _exitPodNotInstalled = 3; @@ -64,7 +65,7 @@ class LintPodspecsCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final List errors = []; final List podspecs = await _podspecsToLint(package); @@ -82,7 +83,7 @@ class LintPodspecsCommand extends PackageLoopingCommand { : PackageResult.fail(errors); } - Future> _podspecsToLint(Directory package) async { + Future> _podspecsToLint(RepositoryPackage package) async { final List podspecs = await getFilesForPackage(package).where((File entity) { final String filePath = entity.path; diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index 29a8ceb12782..e45c09bfd2ef 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_command.dart @@ -6,6 +6,7 @@ import 'package:file/file.dart'; import 'package:platform/platform.dart'; import 'common/plugin_command.dart'; +import 'common/repository_package.dart'; /// A command to list different types of repository content. class ListCommand extends PluginCommand { @@ -39,23 +40,22 @@ class ListCommand extends PluginCommand { Future run() async { switch (getStringArg(_type)) { case _plugin: - await for (final PackageEnumerationEntry package - in getTargetPackages()) { - print(package.directory.path); + await for (final PackageEnumerationEntry entry in getTargetPackages()) { + print(entry.package.path); } break; case _example: - final Stream examples = getTargetPackages() - .map((PackageEnumerationEntry entry) => entry.directory) - .expand(getExamplesForPlugin); - await for (final Directory package in examples) { + final Stream examples = getTargetPackages() + .expand( + (PackageEnumerationEntry entry) => entry.package.getExamples()); + await for (final RepositoryPackage package in examples) { print(package.path); } break; case _package: - await for (final PackageEnumerationEntry package + await for (final PackageEnumerationEntry entry in getTargetPackagesAndSubpackages()) { - print(package.directory.path); + print(entry.package.path); } break; case _file: diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 0bd2ab45f634..725cf23a2e9a 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -10,6 +10,7 @@ import 'common/gradle.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; import 'common/xcode.dart'; const String _unitTestFlag = 'unit'; @@ -115,7 +116,7 @@ this command. } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final List testPlatforms = []; for (final String platform in _requestedPlatforms) { if (pluginSupportsPlatform(platform, package, @@ -171,23 +172,24 @@ this command. : PackageResult.success(); } - Future<_PlatformResult> _testAndroid(Directory plugin, _TestMode mode) async { - bool exampleHasUnitTests(Directory example) { - return example + Future<_PlatformResult> _testAndroid( + RepositoryPackage plugin, _TestMode mode) async { + bool exampleHasUnitTests(RepositoryPackage example) { + return example.directory .childDirectory('android') .childDirectory('app') .childDirectory('src') .childDirectory('test') .existsSync() || - example.parent + example.directory.parent .childDirectory('android') .childDirectory('src') .childDirectory('test') .existsSync(); } - bool exampleHasNativeIntegrationTests(Directory example) { - final Directory integrationTestDirectory = example + bool exampleHasNativeIntegrationTests(RepositoryPackage example) { + final Directory integrationTestDirectory = example.directory .childDirectory('android') .childDirectory('app') .childDirectory('src') @@ -216,12 +218,12 @@ this command. }); } - final Iterable examples = getExamplesForPlugin(plugin); + final Iterable examples = plugin.getExamples(); bool ranTests = false; bool failed = false; bool hasMissingBuild = false; - for (final Directory example in examples) { + for (final RepositoryPackage example in examples) { final bool hasUnitTests = exampleHasUnitTests(example); final bool hasIntegrationTests = exampleHasNativeIntegrationTests(example); @@ -239,11 +241,11 @@ this command. continue; } - final String exampleName = getPackageDescription(example); + final String exampleName = example.displayName; _printRunningExampleTestsMessage(example, 'Android'); final GradleProject project = GradleProject( - example, + example.directory, processRunner: processRunner, platform: platform, ); @@ -301,12 +303,12 @@ this command. return _PlatformResult(RunState.succeeded); } - Future<_PlatformResult> _testIos(Directory plugin, _TestMode mode) { + Future<_PlatformResult> _testIos(RepositoryPackage plugin, _TestMode mode) { return _runXcodeTests(plugin, 'iOS', mode, extraFlags: _iosDestinationFlags); } - Future<_PlatformResult> _testMacOS(Directory plugin, _TestMode mode) { + Future<_PlatformResult> _testMacOS(RepositoryPackage plugin, _TestMode mode) { return _runXcodeTests(plugin, 'macOS', mode); } @@ -316,7 +318,7 @@ this command. /// The tests targets must be added to the Xcode project of the example app, /// usually at "example/{ios,macos}/Runner.xcworkspace". Future<_PlatformResult> _runXcodeTests( - Directory plugin, + RepositoryPackage plugin, String platform, _TestMode mode, { List extraFlags = const [], @@ -330,11 +332,11 @@ this command. // Assume skipped until at least one test has run. RunState overallResult = RunState.skipped; - for (final Directory example in getExamplesForPlugin(plugin)) { - final String exampleName = getPackageDescription(example); + for (final RepositoryPackage example in plugin.getExamples()) { + final String exampleName = example.displayName; if (testTarget != null) { - final Directory project = example + final Directory project = example.directory .childDirectory(platform.toLowerCase()) .childDirectory('Runner.xcodeproj'); final bool? hasTarget = @@ -351,7 +353,7 @@ this command. _printRunningExampleTestsMessage(example, platform); final int exitCode = await _xcode.runXcodeBuild( - example, + example.directory, actions: ['test'], workspace: '${platform.toLowerCase()}/Runner.xcworkspace', scheme: 'Runner', @@ -387,20 +389,22 @@ this command. /// Prints a standard format message indicating that [platform] tests for /// [plugin]'s [example] are about to be run. - void _printRunningExampleTestsMessage(Directory example, String platform) { - print('Running $platform tests for ${getPackageDescription(example)}...'); + void _printRunningExampleTestsMessage( + RepositoryPackage example, String platform) { + print('Running $platform tests for ${example.displayName}...'); } /// Prints a standard format message indicating that no tests were found for /// [plugin]'s [example] for [platform]. - void _printNoExampleTestsMessage(Directory example, String platform) { - print('No $platform tests found for ${getPackageDescription(example)}'); + void _printNoExampleTestsMessage(RepositoryPackage example, String platform) { + print('No $platform tests found for ${example.displayName}'); } } // The type for a function that takes a plugin directory and runs its native // tests for a specific platform. -typedef _TestFunction = Future<_PlatformResult> Function(Directory, _TestMode); +typedef _TestFunction = Future<_PlatformResult> Function( + RepositoryPackage, _TestMode); /// A collection of information related to a specific platform. class _PlatformDetails { diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index fda68a6a74a4..ab9f5f147495 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -16,6 +16,7 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; +import 'common/repository_package.dart'; /// A command to check that packages are publishable via 'dart publish'. class PublishCheckCommand extends PackageLoopingCommand { @@ -75,7 +76,7 @@ class PublishCheckCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final _PublishCheckResult? result = await _passesPublishCheck(package); if (result == null) { return PackageResult.skip('Package is marked as unpublishable.'); @@ -114,8 +115,8 @@ class PublishCheckCommand extends PackageLoopingCommand { } } - Pubspec? _tryParsePubspec(Directory package) { - final File pubspecFile = package.childFile('pubspec.yaml'); + Pubspec? _tryParsePubspec(RepositoryPackage package) { + final File pubspecFile = package.pubspecFile; try { return Pubspec.parse(pubspecFile.readAsStringSync()); @@ -127,12 +128,12 @@ class PublishCheckCommand extends PackageLoopingCommand { } } - Future _hasValidPublishCheckRun(Directory package) async { + Future _hasValidPublishCheckRun(RepositoryPackage package) async { print('Running pub publish --dry-run:'); final io.Process process = await processRunner.start( flutterCommand, ['pub', 'publish', '--', '--dry-run'], - workingDirectory: package, + workingDirectory: package.directory, ); final StringBuffer outputBuffer = StringBuffer(); @@ -183,8 +184,9 @@ class PublishCheckCommand extends PackageLoopingCommand { /// Returns the result of the publish check, or null if the package is marked /// as unpublishable. - Future<_PublishCheckResult?> _passesPublishCheck(Directory package) async { - final String packageName = package.basename; + Future<_PublishCheckResult?> _passesPublishCheck( + RepositoryPackage package) async { + final String packageName = package.directory.basename; final Pubspec? pubspec = _tryParsePubspec(package); if (pubspec == null) { print('no pubspec'); @@ -219,7 +221,7 @@ class PublishCheckCommand extends PackageLoopingCommand { Future<_PublishCheckResult> _checkPublishingStatus( {required String packageName, required Version? version}) async { final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(package: packageName); + await _pubVersionFinder.getPackageVersion(packageName: packageName); switch (pubVersionFinderResponse.result) { case PubVersionFinderResult.success: return pubVersionFinderResponse.versions.contains(version) diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 8bcb9e37e8ef..6e1658f6f6e2 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -140,9 +140,9 @@ class PublishPluginCommand extends PluginCommand { @override Future run() async { - final String package = getStringArg(_packageOption); + final String packageName = getStringArg(_packageOption); final bool publishAllChanged = getBoolArg(_allChangedFlag); - if (package.isEmpty && !publishAllChanged) { + if (packageName.isEmpty && !publishAllChanged) { _print( 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); throw ToolExit(1); @@ -176,7 +176,7 @@ class PublishPluginCommand extends PluginCommand { ); } else { successful = await _publishAndTagPackage( - packageDir: _getPackageDir(package), + packageDir: _getPackageDir(packageName), remoteForTagPush: remote, ); } @@ -202,7 +202,7 @@ class PublishPluginCommand extends PluginCommand { await baseGitDir.runCommand(['tag', '--sort=-committerdate']); final List existingTags = (existingTagsResult.stdout as String) .split('\n') - ..removeWhere((String element) => element.isEmpty); + ..removeWhere((String element) => element.isEmpty); final List packagesReleased = []; final List packagesFailed = []; @@ -307,7 +307,7 @@ Safe to ignore if the package is deleted in this commit. // Check if the package named `packageName` with `version` has already published. final Version version = pubspec.version!; final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(package: pubspec.name); + await _pubVersionFinder.getPackageVersion(packageName: pubspec.name); if (pubVersionFinderResponse.versions.contains(version)) { final String tagsForPackageWithSameVersion = existingTags.firstWhere( (String tag) => @@ -390,8 +390,8 @@ Safe to ignore if the package is deleted in this commit. // Returns the packageDirectory based on the package name. // Throws ToolExit if the `package` doesn't exist. - Directory _getPackageDir(String package) { - final Directory packageDir = packagesDir.childDirectory(package); + Directory _getPackageDir(String packageName) { + final Directory packageDir = packagesDir.childDirectory(packageName); if (!packageDir.existsSync()) { _print('${packageDir.absolute.path} does not exist.'); throw ToolExit(1); diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 0a066ab72baf..def2adaf2788 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -10,6 +10,7 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; /// A command to enforce pubspec conventions across the repository. /// @@ -64,8 +65,8 @@ class PubspecCheckCommand extends PackageLoopingCommand { bool get includeSubpackages => true; @override - Future runForPackage(Directory package) async { - final File pubspec = package.childFile('pubspec.yaml'); + Future runForPackage(RepositoryPackage package) async { + final File pubspec = package.pubspecFile; final bool passesCheck = !pubspec.existsSync() || await _checkPubspec(pubspec, package: package); if (!passesCheck) { @@ -76,7 +77,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { Future _checkPubspec( File pubspecFile, { - required Directory package, + required RepositoryPackage package, }) async { final String contents = pubspecFile.readAsStringSync(); final Pubspec? pubspec = _tryParsePubspec(contents); @@ -154,7 +155,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { List _checkForRepositoryLinkErrors( Pubspec pubspec, { - required Directory package, + required RepositoryPackage package, }) { final List errorMessages = []; if (pubspec.repository == null) { @@ -189,12 +190,12 @@ class PubspecCheckCommand extends PackageLoopingCommand { // Should only be called on plugin packages. String? _checkForImplementsError( Pubspec pubspec, { - required Directory package, + required RepositoryPackage package, }) { if (_isImplementationPackage(package)) { final String? implements = pubspec.flutter!['plugin']!['implements'] as String?; - final String expectedImplements = package.parent.basename; + final String expectedImplements = package.directory.parent.basename; if (implements == null) { return 'Missing "implements: $expectedImplements" in "plugin" section.'; } else if (implements != expectedImplements) { @@ -207,13 +208,13 @@ class PubspecCheckCommand extends PackageLoopingCommand { // Returns true if [packageName] appears to be an implementation package // according to repository conventions. - bool _isImplementationPackage(Directory package) { + bool _isImplementationPackage(RepositoryPackage package) { // An implementation package should be in a group folder... - final Directory parentDir = package.parent; + final Directory parentDir = package.directory.parent; if (parentDir.path == packagesDir.path) { return false; } - final String packageName = package.basename; + final String packageName = package.directory.basename; final String parentName = parentDir.basename; // ... whose name is a prefix of the package name. if (!packageName.startsWith(parentName)) { diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index 9dfe66b7926a..5a0b43d3b223 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -9,6 +9,7 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; /// A command to run Dart unit tests for packages. class TestCommand extends PackageLoopingCommand { @@ -36,13 +37,13 @@ class TestCommand extends PackageLoopingCommand { 'This command requires "flutter" to be in your path.'; @override - Future runForPackage(Directory package) async { - if (!package.childDirectory('test').existsSync()) { + Future runForPackage(RepositoryPackage package) async { + if (!package.directory.childDirectory('test').existsSync()) { return PackageResult.skip('No test/ directory.'); } bool passed; - if (isFlutterPackage(package)) { + if (isFlutterPackage(package.directory)) { passed = await _runFlutterTests(package); } else { passed = await _runDartTests(package); @@ -51,7 +52,7 @@ class TestCommand extends PackageLoopingCommand { } /// Runs the Dart tests for a Flutter package, returning true on success. - Future _runFlutterTests(Directory package) async { + Future _runFlutterTests(RepositoryPackage package) async { final String experiment = getStringArg(kEnableExperiment); final int exitCode = await processRunner.runAndStream( @@ -61,21 +62,21 @@ class TestCommand extends PackageLoopingCommand { '--color', if (experiment.isNotEmpty) '--enable-experiment=$experiment', // TODO(ditman): Remove this once all plugins are migrated to 'drive'. - if (isWebPlugin(package)) '--platform=chrome', + if (pluginSupportsPlatform(kPlatformWeb, package)) '--platform=chrome', ], - workingDir: package, + workingDir: package.directory, ); return exitCode == 0; } /// Runs the Dart tests for a non-Flutter package, returning true on success. - Future _runDartTests(Directory package) async { + Future _runDartTests(RepositoryPackage package) async { // Unlike `flutter test`, `pub run test` does not automatically get // packages int exitCode = await processRunner.runAndStream( 'dart', ['pub', 'get'], - workingDir: package, + workingDir: package.directory, ); if (exitCode != 0) { printError('Unable to fetch dependencies.'); @@ -92,7 +93,7 @@ class TestCommand extends PackageLoopingCommand { if (experiment.isNotEmpty) '--enable-experiment=$experiment', 'test', ], - workingDir: package, + workingDir: package.directory, ); return exitCode == 0; diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 67c563782888..67a81b967a8e 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -16,6 +16,7 @@ import 'common/git_version_finder.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; +import 'common/repository_package.dart'; /// Categories of version change types. enum NextVersionType { @@ -133,7 +134,7 @@ class VersionCheckCommand extends PackageLoopingCommand { Future initializeRun() async {} @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final Pubspec? pubspec = _tryParsePubspec(package); if (pubspec == null) { // No remaining checks make sense, so fail immediately. @@ -196,7 +197,7 @@ class VersionCheckCommand extends PackageLoopingCommand { /// the name from pubspec.yaml, not the on disk name if different.) Future _fetchPreviousVersionFromPub(String packageName) async { final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(package: packageName); + await _pubVersionFinder.getPackageVersion(packageName: packageName); switch (pubVersionFinderResponse.result) { case PubVersionFinderResult.success: return pubVersionFinderResponse.versions.first; @@ -214,10 +215,10 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} /// Returns the version of [package] from git at the base comparison hash. Future _getPreviousVersionFromGit( - Directory package, { + RepositoryPackage package, { required GitVersionFinder gitVersionFinder, }) async { - final File pubspecFile = package.childFile('pubspec.yaml'); + final File pubspecFile = package.pubspecFile; final String relativePath = path.relative(pubspecFile.absolute.path, from: (await gitDir).path); // Use Posix-style paths for git. @@ -230,7 +231,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} /// Returns the state of the verison of [package] relative to the comparison /// base (git or pub, depending on flags). Future<_CurrentVersionState> _getVersionState( - Directory package, { + RepositoryPackage package, { required Pubspec pubspec, }) async { // This method isn't called unless `version` is non-null. @@ -310,7 +311,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} /// /// Returns false if the CHANGELOG fails validation. Future _validateChangelogVersion( - Directory package, { + RepositoryPackage package, { required Pubspec pubspec, required bool pubspecVersionChanged, }) async { @@ -318,7 +319,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} final Version fromPubspec = pubspec.version!; // get first version from CHANGELOG - final File changelog = package.childFile('CHANGELOG.md'); + final File changelog = package.directory.childFile('CHANGELOG.md'); final List lines = changelog.readAsLinesSync(); String? firstLineWithText; final Iterator iterator = lines.iterator; @@ -386,8 +387,8 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. return true; } - Pubspec? _tryParsePubspec(Directory package) { - final File pubspecFile = package.childFile('pubspec.yaml'); + Pubspec? _tryParsePubspec(RepositoryPackage package) { + final File pubspecFile = package.pubspecFile; try { final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart index 27cd8c435142..3d34dab9f087 100644 --- a/script/tool/lib/src/xcode_analyze_command.dart +++ b/script/tool/lib/src/xcode_analyze_command.dart @@ -9,6 +9,7 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; import 'common/xcode.dart'; /// The command to run Xcode's static analyzer on plugins. @@ -42,7 +43,7 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final bool testIos = getBoolArg(kPlatformIos) && pluginSupportsPlatform(kPlatformIos, package, requiredMode: PlatformSupport.inline); @@ -78,18 +79,18 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { /// Analyzes [plugin] for [platform], returning true if it passed analysis. Future _analyzePlugin( - Directory plugin, + RepositoryPackage plugin, String platform, { List extraFlags = const [], }) async { bool passing = true; - for (final Directory example in getExamplesForPlugin(plugin)) { + for (final RepositoryPackage example in plugin.getExamples()) { // Running tests and static analyzer. - final String examplePath = - getRelativePosixPath(example, from: plugin.parent); + final String examplePath = getRelativePosixPath(example.directory, + from: plugin.directory.parent); print('Running $platform tests and analyzer for $examplePath...'); final int exitCode = await _xcode.runXcodeBuild( - example, + example.directory, actions: ['analyze'], workspace: '${platform.toLowerCase()}/Runner.xcworkspace', scheme: 'Runner', diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 00e64ddc21fe..721923ae9c6e 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -11,6 +11,7 @@ import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/package_looping_command.dart'; import 'package:flutter_plugin_tools/src/common/process_runner.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:git/git.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; @@ -578,64 +579,6 @@ void main() { ])); }); }); - - group('utility', () { - test('getPackageDescription prints packageDir-relative paths by default', - () async { - final TestPackageLoopingCommand command = - TestPackageLoopingCommand(packagesDir, platform: mockPlatform); - - expect( - command.getPackageDescription(packagesDir.childDirectory('foo')), - 'foo', - ); - expect( - command.getPackageDescription(packagesDir - .childDirectory('foo') - .childDirectory('bar') - .childDirectory('baz')), - 'foo/bar/baz', - ); - }); - - test('getPackageDescription always uses Posix-style paths', () async { - mockPlatform.isWindows = true; - final TestPackageLoopingCommand command = - TestPackageLoopingCommand(packagesDir, platform: mockPlatform); - - expect( - command.getPackageDescription(packagesDir.childDirectory('foo')), - 'foo', - ); - expect( - command.getPackageDescription(packagesDir - .childDirectory('foo') - .childDirectory('bar') - .childDirectory('baz')), - 'foo/bar/baz', - ); - }); - - test( - 'getPackageDescription elides group name in grouped federated plugin structure', - () async { - final TestPackageLoopingCommand command = - TestPackageLoopingCommand(packagesDir, platform: mockPlatform); - - expect( - command.getPackageDescription(packagesDir - .childDirectory('a_plugin') - .childDirectory('a_plugin_platform_interface')), - 'a_plugin_platform_interface', - ); - expect( - command.getPackageDescription(packagesDir - .childDirectory('a_plugin') - .childDirectory('a_plugin_web')), - 'a_plugin_web', - ); - }); - }); } class TestPackageLoopingCommand extends PackageLoopingCommand { @@ -699,18 +642,18 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { checkedPackages.add(package.path); - final File warningFile = package.childFile(_warningFile); + final File warningFile = package.directory.childFile(_warningFile); if (warningFile.existsSync()) { final List warnings = warningFile.readAsLinesSync(); warnings.forEach(logWarning); } - final File skipFile = package.childFile(_skipFile); + final File skipFile = package.directory.childFile(_skipFile); if (skipFile.existsSync()) { return PackageResult.skip(skipFile.readAsStringSync()); } - final File errorFile = package.childFile(_errorFile); + final File errorFile = package.directory.childFile(_errorFile); if (errorFile.existsSync()) { return PackageResult.fail(errorFile.readAsLinesSync()); } diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 2f332aa8eb55..10bdff4e9c56 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -498,7 +498,7 @@ packages/plugin3/plugin3.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory package) => package.path) + .map((Directory packageDir) => packageDir.path) .toList())); } }); @@ -541,7 +541,7 @@ packages/plugin3/plugin3.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory package) => package.path) + .map((Directory packageDir) => packageDir.path) .toList())); } }); @@ -594,7 +594,7 @@ packages/plugin3/plugin3.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory package) => package.path) + .map((Directory packageDir) => packageDir.path) .toList())); } }); @@ -620,8 +620,8 @@ class SamplePluginCommand extends PluginCommand { @override Future run() async { - await for (final PackageEnumerationEntry package in getTargetPackages()) { - plugins.add(package.directory.path); + await for (final PackageEnumerationEntry entry in getTargetPackages()) { + plugins.add(entry.package.path); } } } diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart index c32c3f8e02bf..7f1ba2add00a 100644 --- a/script/tool/test/common/plugin_utils_test.dart +++ b/script/tool/test/common/plugin_utils_test.dart @@ -6,6 +6,7 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:test/test.dart'; import '../util.dart'; @@ -21,7 +22,8 @@ void main() { group('pluginSupportsPlatform', () { test('no platforms', () async { - final Directory plugin = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = + RepositoryPackage(createFakePlugin('plugin', packagesDir)); expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isFalse); expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); @@ -32,7 +34,8 @@ void main() { }); test('all platforms', () async { - final Directory plugin = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', packagesDir, platformSupport: { kPlatformAndroid: PlatformSupport.inline, kPlatformIos: PlatformSupport.inline, @@ -40,7 +43,7 @@ void main() { kPlatformMacos: PlatformSupport.inline, kPlatformWeb: PlatformSupport.inline, kPlatformWindows: PlatformSupport.inline, - }); + })); expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); expect(pluginSupportsPlatform(kPlatformIos, plugin), isTrue); @@ -51,7 +54,7 @@ void main() { }); test('some platforms', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -59,7 +62,7 @@ void main() { kPlatformLinux: PlatformSupport.inline, kPlatformWeb: PlatformSupport.inline, }, - ); + )); expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); @@ -70,7 +73,7 @@ void main() { }); test('inline plugins are only detected as inline', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -81,7 +84,7 @@ void main() { kPlatformWeb: PlatformSupport.inline, kPlatformWindows: PlatformSupport.inline, }, - ); + )); expect( pluginSupportsPlatform(kPlatformAndroid, plugin, @@ -135,7 +138,7 @@ void main() { test('federated plugins are only detected as federated', () async { const String pluginName = 'plugin'; - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( pluginName, packagesDir, platformSupport: { @@ -146,7 +149,7 @@ void main() { kPlatformWeb: PlatformSupport.federated, kPlatformWindows: PlatformSupport.federated, }, - ); + )); expect( pluginSupportsPlatform(kPlatformAndroid, plugin, diff --git a/script/tool/test/common/pub_version_finder_test.dart b/script/tool/test/common/pub_version_finder_test.dart index 7d8658a907ee..1692cf214abe 100644 --- a/script/tool/test/common/pub_version_finder_test.dart +++ b/script/tool/test/common/pub_version_finder_test.dart @@ -19,7 +19,7 @@ void main() { }); final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); final PubVersionFinderResponse response = - await finder.getPackageVersion(package: 'some_package'); + await finder.getPackageVersion(packageName: 'some_package'); expect(response.versions, isEmpty); expect(response.result, PubVersionFinderResult.noPackageFound); @@ -33,7 +33,7 @@ void main() { }); final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); final PubVersionFinderResponse response = - await finder.getPackageVersion(package: 'some_package'); + await finder.getPackageVersion(packageName: 'some_package'); expect(response.versions, isEmpty); expect(response.result, PubVersionFinderResult.fail); @@ -64,7 +64,7 @@ void main() { }); final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); final PubVersionFinderResponse response = - await finder.getPackageVersion(package: 'some_package'); + await finder.getPackageVersion(packageName: 'some_package'); expect(response.versions, [ Version.parse('2.0.0'), diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart new file mode 100644 index 000000000000..5c5624312f51 --- /dev/null +++ b/script/tool/test/common/repository_package_test.dart @@ -0,0 +1,123 @@ +// 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:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; +import 'package:test/test.dart'; + +import '../util.dart'; + +void main() { + late FileSystem fileSystem; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + }); + + group('displayName', () { + test('prints packageDir-relative paths by default', () async { + expect( + RepositoryPackage(packagesDir.childDirectory('foo')).displayName, + 'foo', + ); + expect( + RepositoryPackage(packagesDir + .childDirectory('foo') + .childDirectory('bar') + .childDirectory('baz')) + .displayName, + 'foo/bar/baz', + ); + }); + + test('handles third_party/packages/', () async { + expect( + RepositoryPackage(packagesDir.parent + .childDirectory('third_party') + .childDirectory('packages') + .childDirectory('foo') + .childDirectory('bar') + .childDirectory('baz')) + .displayName, + 'foo/bar/baz', + ); + }); + + test('always uses Posix-style paths', () async { + final Directory windowsPackagesDir = createPackagesDirectory( + fileSystem: MemoryFileSystem(style: FileSystemStyle.windows)); + + expect( + RepositoryPackage(windowsPackagesDir.childDirectory('foo')).displayName, + 'foo', + ); + expect( + RepositoryPackage(windowsPackagesDir + .childDirectory('foo') + .childDirectory('bar') + .childDirectory('baz')) + .displayName, + 'foo/bar/baz', + ); + }); + + test('elides group name in grouped federated plugin structure', () async { + expect( + RepositoryPackage(packagesDir + .childDirectory('a_plugin') + .childDirectory('a_plugin_platform_interface')) + .displayName, + 'a_plugin_platform_interface', + ); + expect( + RepositoryPackage(packagesDir + .childDirectory('a_plugin') + .childDirectory('a_plugin_platform_web')) + .displayName, + 'a_plugin_platform_web', + ); + }); + + // The app-facing package doesn't get elided to avoid potential confusion + // with the group folder itself. + test('does not elide group name for app-facing packages', () async { + expect( + RepositoryPackage(packagesDir + .childDirectory('a_plugin') + .childDirectory('a_plugin')) + .displayName, + 'a_plugin/a_plugin', + ); + }); + }); + + group('getExamples', () { + test('handles a single example', () async { + final Directory plugin = createFakePlugin('a_plugin', packagesDir); + + final List examples = + RepositoryPackage(plugin).getExamples().toList(); + + expect(examples.length, 1); + expect(examples[0].path, plugin.childDirectory('example').path); + }); + + test('handles multiple examples', () async { + final Directory plugin = createFakePlugin('a_plugin', packagesDir, + examples: ['example1', 'example2']); + + final List examples = + RepositoryPackage(plugin).getExamples().toList(); + + expect(examples.length, 2); + expect(examples[0].path, + plugin.childDirectory('example').childDirectory('example1').path); + expect(examples[1].path, + plugin.childDirectory('example').childDirectory('example2').path); + }); + }); +} diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index cf57a9d0dcf7..e2bf1e3e6e8e 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -49,10 +49,10 @@ void main() { /// Returns a modified version of a list of [relativePaths] that are relative /// to [package] to instead be relative to [packagesDir]. List _getPackagesDirRelativePaths( - Directory package, List relativePaths) { + Directory packageDir, List relativePaths) { final p.Context path = analyzeCommand.path; final String relativeBase = - path.relative(package.path, from: packagesDir.path); + path.relative(packageDir.path, from: packagesDir.path); return relativePaths .map((String relativePath) => path.join(relativeBase, relativePath)) .toList(); diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 10a85f49e815..05aebe82fd79 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -46,6 +46,7 @@ Directory createPackagesDirectory( /// /// [extraFiles] is an optional list of plugin-relative paths, using Posix /// separators, of extra files to create in the plugin. +// TODO(stuartmorgan): Convert the return to a RepositoryPackage. Directory createFakePlugin( String name, Directory parentDirectory, { @@ -77,6 +78,7 @@ Directory createFakePlugin( /// /// [extraFiles] is an optional list of package-relative paths, using unix-style /// separators, of extra files to create in the package. +// TODO(stuartmorgan): Convert the return to a RepositoryPackage. Directory createFakePackage( String name, Directory parentDirectory, { From 729c3e4117e6d1a026b50f363b2a202352231fdd Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 24 Aug 2021 21:22:36 -0400 Subject: [PATCH 089/123] [flutter_plugin_tool] Migrate publish_plugin_command_test to runCapturingPrint (#4260) Finishes the migration of tool tests to `runCapturingPrint`. This makes the tests much less verbose, and makes it match the rest of the tool tests. Also adds the use of `printError` for error output, now that it's trivial to do so. --- .../tool/lib/src/publish_plugin_command.dart | 70 ++-- .../test/publish_plugin_command_test.dart | 355 ++++++++++-------- 2 files changed, 239 insertions(+), 186 deletions(-) diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 6e1658f6f6e2..5a75ce6af89f 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -11,6 +11,7 @@ import 'package:git/git.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; @@ -48,15 +49,15 @@ class PublishPluginCommand extends PluginCommand { PublishPluginCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - Print print = print, + Platform platform = const LocalPlatform(), io.Stdin? stdinput, GitDir? gitDir, http.Client? httpClient, }) : _pubVersionFinder = PubVersionFinder(httpClient: httpClient ?? http.Client()), - _print = print, _stdin = stdinput ?? io.stdin, - super(packagesDir, processRunner: processRunner, gitDir: gitDir) { + super(packagesDir, + platform: platform, processRunner: processRunner, gitDir: gitDir) { argParser.addOption( _packageOption, help: 'The package to publish.' @@ -133,7 +134,6 @@ class PublishPluginCommand extends PluginCommand { 'If running this on CI, an environment variable named $_pubCredentialName must be set to a String that represents the pub credential JSON.\n' 'WARNING: Do not check in the content of pub credential JSON, it should only come from secure sources.'; - final Print _print; final io.Stdin _stdin; StreamSubscription? _stdinSubscription; final PubVersionFinder _pubVersionFinder; @@ -143,12 +143,12 @@ class PublishPluginCommand extends PluginCommand { final String packageName = getStringArg(_packageOption); final bool publishAllChanged = getBoolArg(_allChangedFlag); if (packageName.isEmpty && !publishAllChanged) { - _print( + printError( 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); throw ToolExit(1); } - _print('Checking local repo...'); + print('Checking local repo...'); final GitDir repository = await gitDir; final bool shouldPushTag = getBoolArg(_pushTagsOption); @@ -163,9 +163,9 @@ class PublishPluginCommand extends PluginCommand { } remote = _RemoteInfo(name: remoteName, url: remoteUrl); } - _print('Local repo is ready!'); + print('Local repo is ready!'); if (getBoolArg(_dryRunFlag)) { - _print('=============== DRY RUN ==============='); + print('=============== DRY RUN ==============='); } bool successful; @@ -193,11 +193,11 @@ class PublishPluginCommand extends PluginCommand { final List changedPubspecs = await gitVersionFinder.getChangedPubSpecs(); if (changedPubspecs.isEmpty) { - _print('No version updates in this commit.'); + print('No version updates in this commit.'); return true; } - _print('Getting existing tags...'); + print('Getting existing tags...'); final io.ProcessResult existingTagsResult = await baseGitDir.runCommand(['tag', '--sort=-committerdate']); final List existingTags = (existingTagsResult.stdout as String) @@ -228,7 +228,7 @@ class PublishPluginCommand extends PluginCommand { packagesFailed.add(pubspecFile.parent.basename); continue; } - _print('\n'); + print('\n'); if (await _publishAndTagPackage( packageDir: pubspecFile.parent, remoteForTagPush: remoteForTagPush, @@ -237,13 +237,13 @@ class PublishPluginCommand extends PluginCommand { } else { packagesFailed.add(pubspecFile.parent.basename); } - _print('\n'); + print('\n'); } if (packagesReleased.isNotEmpty) { - _print('Packages released: ${packagesReleased.join(', ')}'); + print('Packages released: ${packagesReleased.join(', ')}'); } if (packagesFailed.isNotEmpty) { - _print( + printError( 'Failed to release the following packages: ${packagesFailed.join(', ')}, see above for details.'); } return packagesFailed.isEmpty; @@ -268,7 +268,7 @@ class PublishPluginCommand extends PluginCommand { return false; } } - _print('Released [${packageDir.basename}] successfully.'); + print('Released [${packageDir.basename}] successfully.'); return true; } @@ -278,7 +278,7 @@ class PublishPluginCommand extends PluginCommand { required List existingTags, }) async { if (!pubspecFile.existsSync()) { - _print(''' + print(''' The file at The pubspec file at ${pubspecFile.path} does not exist. Publishing will not happen for ${pubspecFile.parent.basename}. Safe to ignore if the package is deleted in this commit. '''); @@ -299,7 +299,7 @@ Safe to ignore if the package is deleted in this commit. } if (pubspec.version == null) { - _print( + printError( 'No version found. A package that intentionally has no version should be marked "publish_to: none"'); return _CheckNeedsReleaseResult.failure; } @@ -314,14 +314,14 @@ Safe to ignore if the package is deleted in this commit. tag.split('-v').first == pubspec.name && tag.split('-v').last == version.toString(), orElse: () => ''); - _print( + print( 'The version $version of ${pubspec.name} has already been published'); if (tagsForPackageWithSameVersion.isEmpty) { - _print( + printError( 'However, the git release tag for this version (${pubspec.name}-v$version) is not found. Please manually fix the tag then run the command again.'); return _CheckNeedsReleaseResult.failure; } else { - _print('skip.'); + print('skip.'); return _CheckNeedsReleaseResult.noRelease; } } @@ -340,7 +340,7 @@ Safe to ignore if the package is deleted in this commit. if (!publishOK) { return false; } - _print('Package published!'); + print('Package published!'); return true; } @@ -353,7 +353,7 @@ Safe to ignore if the package is deleted in this commit. _RemoteInfo? remoteForPush, }) async { final String tag = _getTag(packageDir); - _print('Tagging release $tag...'); + print('Tagging release $tag...'); if (!getBoolArg(_dryRunFlag)) { final io.ProcessResult result = await processRunner.run( 'git', @@ -370,7 +370,7 @@ Safe to ignore if the package is deleted in this commit. return true; } - _print('Pushing tag to ${remoteForPush.name}...'); + print('Pushing tag to ${remoteForPush.name}...'); return await _pushTagToRemote( tag: tag, remote: remoteForPush, @@ -381,9 +381,9 @@ Safe to ignore if the package is deleted in this commit. await _stdinSubscription?.cancel(); _stdinSubscription = null; if (successful) { - _print('Done!'); + print('Done!'); } else { - _print('Failed, see above for details.'); + printError('Failed, see above for details.'); throw ToolExit(1); } } @@ -393,7 +393,7 @@ Safe to ignore if the package is deleted in this commit. Directory _getPackageDir(String packageName) { final Directory packageDir = packagesDir.childDirectory(packageName); if (!packageDir.existsSync()) { - _print('${packageDir.absolute.path} does not exist.'); + printError('${packageDir.absolute.path} does not exist.'); throw ToolExit(1); } return packageDir; @@ -412,7 +412,7 @@ Safe to ignore if the package is deleted in this commit. final String statusOutput = statusResult.stdout as String; if (statusOutput.isNotEmpty) { - _print( + printError( "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" '$statusOutput\n' 'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.'); @@ -435,7 +435,7 @@ Safe to ignore if the package is deleted in this commit. Future _publish(Directory packageDir) async { final List publishFlags = getStringListArg(_pubFlagsOption); - _print( + print( 'Running `pub publish ${publishFlags.join(' ')}` in ${packageDir.absolute.path}...\n'); if (getBoolArg(_dryRunFlag)) { return true; @@ -451,18 +451,14 @@ Safe to ignore if the package is deleted in this commit. final io.Process publish = await processRunner.start( flutterCommand, ['pub', 'publish'] + publishFlags, workingDirectory: packageDir); - publish.stdout - .transform(utf8.decoder) - .listen((String data) => _print(data)); - publish.stderr - .transform(utf8.decoder) - .listen((String data) => _print(data)); + publish.stdout.transform(utf8.decoder).listen((String data) => print(data)); + publish.stderr.transform(utf8.decoder).listen((String data) => print(data)); _stdinSubscription ??= _stdin .transform(utf8.decoder) .listen((String data) => publish.stdin.writeln(data)); final int result = await publish.exitCode; if (result != 0) { - _print('Publish ${packageDir.basename} failed.'); + printError('Publish ${packageDir.basename} failed.'); return false; } return true; @@ -490,10 +486,10 @@ Safe to ignore if the package is deleted in this commit. }) async { assert(remote != null && tag != null); if (!getBoolArg(_skipConfirmationFlag)) { - _print('Ready to push $tag to ${remote.url} (y/n)?'); + print('Ready to push $tag to ${remote.url} (y/n)?'); final String? input = _stdin.readLineSync(); if (input?.toLowerCase() != 'y') { - _print('Tag push canceled.'); + print('Tag push canceled.'); return false; } } diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 9a937daa2384..576d3a4c88c8 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -24,7 +24,6 @@ import 'util.dart'; void main() { const String testPluginName = 'foo'; - late List printedMessages; late Directory testRoot; late Directory packagesDir; @@ -62,13 +61,9 @@ void main() { await gitDir.runCommand(['commit', '-m', 'Initial commit']); processRunner = TestProcessRunner(); mockStdin = MockStdin(); - printedMessages = []; commandRunner = CommandRunner('tester', '') ..addCommand(PublishPluginCommand(packagesDir, - processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), - stdinput: mockStdin, - gitDir: gitDir)); + processRunner: processRunner, stdinput: mockStdin, gitDir: gitDir)); }); tearDown(() { @@ -77,50 +72,66 @@ void main() { group('Initial validation', () { test('requires a package flag', () async { - await expectLater(() => commandRunner.run(['publish-plugin']), - throwsA(isA())); - expect( - printedMessages.last, contains('Must specify a package to publish.')); + Error? commandError; + final List output = await runCapturingPrint( + commandRunner, ['publish-plugin'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect(output.last, contains('Must specify a package to publish.')); }); test('requires an existing flag', () async { - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - 'iamerror', - '--no-push-tags' - ]), - throwsA(isA())); - - expect(printedMessages.last, contains('iamerror does not exist')); + Error? commandError; + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--package', 'iamerror', '--no-push-tags'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect(output.last, contains('iamerror does not exist')); }); test('refuses to proceed with dirty files', () async { pluginDir.childFile('tmp').createSync(); - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags' - ]), - throwsA(isA())); + Error? commandError; + final List output = await runCapturingPrint( + commandRunner, [ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags' + ], errorHandler: (Error e) { + commandError = e; + }); + expect(commandError, isA()); expect( - printedMessages, - containsAllInOrder([ - 'There are files in the package directory that haven\'t been saved in git. Refusing to publish these files:\n\n?? packages/foo/tmp\n\nIf the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.', - 'Failed, see above for details.', + output, + containsAllInOrder([ + contains('There are files in the package directory that haven\'t ' + 'been saved in git. Refusing to publish these files:\n\n' + '?? packages/foo/tmp\n\n' + 'If the directory should be clean, you can run `git clean -xdf && ' + 'git reset --hard HEAD` to wipe all local changes.'), + contains('Failed, see above for details.'), ])); }); test('fails immediately if the remote doesn\'t exist', () async { - await expectLater( - () => commandRunner - .run(['publish-plugin', '--package', testPluginName]), - throwsA(isA())); + Error? commandError; + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--package', + testPluginName + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect(processRunner.results.last.stderr, contains('No such remote')); }); @@ -128,7 +139,8 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; - await commandRunner.run([ + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -136,7 +148,7 @@ void main() { '--no-tag-release' ]); - expect(printedMessages.last, 'Done!'); + expect(output.last, 'Done!'); }); test('can publish non-flutter package', () async { @@ -149,20 +161,28 @@ void main() { await gitDir.runCommand(['commit', '-m', 'Initial commit']); // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; - await commandRunner.run([ + + final List output = await runCapturingPrint( + commandRunner, [ 'publish-plugin', '--package', packageName, '--no-push-tags', '--no-tag-release' ]); - expect(printedMessages.last, 'Done!'); + + expect(output.last, 'Done!'); }); }); group('Publishes package', () { test('while showing all output from pub publish to the user', () async { - final Future publishCommand = commandRunner.run([ + processRunner.mockPublishStdout = 'Foo'; + processRunner.mockPublishStderr = 'Bar'; + processRunner.mockPublishCompleteCode = 0; + + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -170,28 +190,21 @@ void main() { '--no-tag-release' ]); - processRunner.mockPublishStdout = 'Foo'; - processRunner.mockPublishStderr = 'Bar'; - processRunner.mockPublishCompleteCode = 0; - - await publishCommand; - - expect(printedMessages, contains('Foo')); - expect(printedMessages, contains('Bar')); + expect(output, contains('Foo')); + expect(output, contains('Bar')); }); test('forwards input from the user to `pub publish`', () async { - final Future publishCommand = commandRunner.run([ + mockStdin.mockUserInputs.add(utf8.encode('user input')); + processRunner.mockPublishCompleteCode = 0; + + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, '--no-push-tags', '--no-tag-release' ]); - mockStdin.mockUserInputs.add(utf8.encode('user input')); - processRunner.mockPublishCompleteCode = 0; - - await publishCommand; expect(processRunner.mockPublishProcess.stdinMock.lines, contains('user input')); @@ -199,7 +212,8 @@ void main() { test('forwards --pub-publish-flags to pub publish', () async { processRunner.mockPublishCompleteCode = 0; - await commandRunner.run([ + + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -221,7 +235,8 @@ void main() { () async { processRunner.mockPublishCompleteCode = 0; _createMockCredentialFile(); - await commandRunner.run([ + + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -241,23 +256,30 @@ void main() { test('throws if pub publish fails', () async { processRunner.mockPublishCompleteCode = 128; - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release', - ]), - throwsA(isA())); - - expect(printedMessages, contains('Publish foo failed.')); + + Error? commandError; + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Publish foo failed.'), + ])); }); test('publish, dry run', () async { - // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. - processRunner.mockPublishCompleteCode = 1; - await commandRunner.run([ + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -268,7 +290,7 @@ void main() { expect(processRunner.pushTagsArgs, isEmpty); expect( - printedMessages, + output, containsAllInOrder([ '=============== DRY RUN ===============', 'Running `pub publish ` in ${pluginDir.path}...\n', @@ -280,7 +302,8 @@ void main() { group('Tags release', () { test('with the version and name from the pubspec.yaml', () async { processRunner.mockPublishCompleteCode = 0; - await commandRunner.run([ + + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -295,16 +318,24 @@ void main() { test('only if publishing succeeded', () async { processRunner.mockPublishCompleteCode = 128; - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - ]), - throwsA(isA())); - - expect(printedMessages, contains('Publish foo failed.')); + + Error? commandError; + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Publish foo failed.'), + ])); final String? tag = (await gitDir.runCommand( ['show-ref', '$testPluginName-v0.0.1'], throwOnError: false)) @@ -322,22 +353,28 @@ void main() { test('requires user confirmation', () async { processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'help'; - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - ]), - throwsA(isA())); - - expect(printedMessages, contains('Tag push canceled.')); + + Error? commandError; + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--package', + testPluginName, + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect(output, contains('Tag push canceled.')); }); test('to upstream by default', () async { await gitDir.runCommand(['tag', 'garbage']); processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner.run([ + + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -346,7 +383,7 @@ void main() { expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); expect(processRunner.pushTagsArgs[1], 'upstream'); expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(printedMessages.last, 'Done!'); + expect(output.last, 'Done!'); }); test('does not ask for user input if the --skip-confirmation flag is on', @@ -354,7 +391,9 @@ void main() { await gitDir.runCommand(['tag', 'garbage']); processRunner.mockPublishCompleteCode = 0; _createMockCredentialFile(); - await commandRunner.run([ + + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--skip-confirmation', '--package', @@ -364,7 +403,7 @@ void main() { expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); expect(processRunner.pushTagsArgs[1], 'upstream'); expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(printedMessages.last, 'Done!'); + expect(output.last, 'Done!'); }); test('to upstream by default, dry run', () async { @@ -372,12 +411,13 @@ void main() { // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. processRunner.mockPublishCompleteCode = 1; mockStdin.readLineOutput = 'y'; - await commandRunner.run( + + final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--package', testPluginName, '--dry-run']); expect(processRunner.pushTagsArgs, isEmpty); expect( - printedMessages, + output, containsAllInOrder([ '=============== DRY RUN ===============', 'Running `pub publish ` in ${pluginDir.path}...\n', @@ -392,7 +432,9 @@ void main() { ['remote', 'add', 'origin', 'http://localhost:8001']); processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner.run([ + + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -403,12 +445,14 @@ void main() { expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); expect(processRunner.pushTagsArgs[1], 'origin'); expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(printedMessages.last, 'Done!'); + expect(output.last, 'Done!'); }); test('only if tagging and pushing to remotes are both enabled', () async { processRunner.mockPublishCompleteCode = 0; - await commandRunner.run([ + + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -416,7 +460,7 @@ void main() { ]); expect(processRunner.pushTagsArgs.isEmpty, isTrue); - expect(printedMessages.last, 'Done!'); + expect(output.last, 'Done!'); }); }); @@ -450,7 +494,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -473,10 +516,12 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -523,7 +568,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -543,8 +587,10 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + processRunner.pushTagsArgs.clear(); // Non-federated @@ -554,11 +600,12 @@ void main() { createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); - // Immediately return 0 when running `pub publish`. - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + output.addAll(await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~'])); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -597,7 +644,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -617,14 +663,17 @@ void main() { // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. processRunner.mockPublishCompleteCode = 1; mockStdin.readLineOutput = 'y'; - await commandRunner.run([ + + final List output = await runCapturingPrint( + commandRunner, [ 'publish-plugin', '--all-changed', '--base-sha=HEAD~', '--dry-run' ]); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -662,7 +711,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -683,10 +731,12 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -704,7 +754,6 @@ void main() { expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); processRunner.pushTagsArgs.clear(); - printedMessages.clear(); final List plugin1Pubspec = pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); @@ -724,10 +773,10 @@ void main() { await gitDir .runCommand(['commit', '-m', 'Update versions to 0.0.2']); - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + final List output2 = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( - printedMessages, + output2, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -769,7 +818,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -790,10 +838,11 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -811,7 +860,6 @@ void main() { expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); processRunner.pushTagsArgs.clear(); - printedMessages.clear(); final List plugin1Pubspec = pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); @@ -830,10 +878,10 @@ void main() { 'Update plugin1 versions to 0.0.2, delete plugin2' ]); - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + final List output2 = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( - printedMessages, + output2, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -872,7 +920,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -895,10 +942,12 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -935,7 +984,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -956,10 +1004,17 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await expectLater( - () => commandRunner.run( - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']), - throwsA(isA())); + + Error? commandError; + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--all-changed', + '--base-sha=HEAD~' + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect(processRunner.pushTagsArgs, isEmpty); }); @@ -984,10 +1039,12 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -1011,7 +1068,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -1029,23 +1085,24 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', 'Done!' ])); expect( - printedMessages.contains( + output.contains( 'Running `pub publish ` in ${flutterPluginTools.path}...\n', ), isFalse); expect(processRunner.pushTagsArgs, isEmpty); processRunner.pushTagsArgs.clear(); - printedMessages.clear(); }); }); } From 5b5f8016d31a172470ae5da49f2c0b57e2fe2481 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Wed, 25 Aug 2021 16:41:09 +0200 Subject: [PATCH 090/123] [camera] Expand CameraImage DTO with properties for lens aperture, exposure time and ISO. (#4256) --- packages/camera/camera/CHANGELOG.md | 4 ++ .../io/flutter/plugins/camera/Camera.java | 11 ++- .../plugins/camera/CameraCaptureCallback.java | 21 +++++- .../camera/types/CameraCaptureProperties.java | 67 +++++++++++++++++ .../CameraCaptureCallbackStatesTest.java | 6 +- .../camera/CameraCaptureCallbackTest.java | 72 +++++++++++++++++++ .../camera/camera/ios/Classes/CameraPlugin.m | 5 ++ .../camera/camera/lib/src/camera_image.dart | 14 ++++ packages/camera/camera/pubspec.yaml | 2 +- .../camera/camera/test/camera_image_test.dart | 15 ++++ 10 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 68188d6510ff..73cce2c539c1 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.1 + +* Added `lensAperture`, `sensorExposureTime` and `sensorSensitivity` properties to the `CameraImage` dto. + ## 0.9.0 * Complete rewrite of Android plugin to fix many capture, focus, flash, orientation and exposure issues. diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 4724d22a1bcd..43479aca616c 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -61,6 +61,7 @@ import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; import io.flutter.plugins.camera.media.MediaRecorderBuilder; +import io.flutter.plugins.camera.types.CameraCaptureProperties; import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; import io.flutter.view.TextureRegistry.SurfaceTextureEntry; import java.io.File; @@ -130,6 +131,8 @@ class Camera /** Holds the current capture timeouts */ private CaptureTimeoutsWrapper captureTimeouts; + /** Holds the last known capture properties */ + private CameraCaptureProperties captureProps; private MethodChannel.Result flutterResult; @@ -158,7 +161,8 @@ public Camera( // Create capture callback. captureTimeouts = new CaptureTimeoutsWrapper(3000, 3000); - cameraCaptureCallback = CameraCaptureCallback.create(this, captureTimeouts); + captureProps = new CameraCaptureProperties(); + cameraCaptureCallback = CameraCaptureCallback.create(this, captureTimeouts, captureProps); startBackgroundThread(); } @@ -1042,6 +1046,11 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i imageBuffer.put("height", img.getHeight()); imageBuffer.put("format", img.getFormat()); imageBuffer.put("planes", planes); + imageBuffer.put("lensAperture", this.captureProps.getLastLensAperture()); + imageBuffer.put("sensorExposureTime", this.captureProps.getLastSensorExposureTime()); + Integer sensorSensitivity = this.captureProps.getLastSensorSensitivity(); + imageBuffer.put( + "sensorSensitivity", sensorSensitivity == null ? null : (double) sensorSensitivity); final Handler handler = new Handler(Looper.getMainLooper()); handler.post(() -> imageStreamSink.success(imageBuffer)); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java index 21dcb602655d..805f18298958 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java @@ -11,6 +11,7 @@ import android.hardware.camera2.TotalCaptureResult; import android.util.Log; import androidx.annotation.NonNull; +import io.flutter.plugins.camera.types.CameraCaptureProperties; import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; /** @@ -22,13 +23,16 @@ class CameraCaptureCallback extends CaptureCallback { private final CameraCaptureStateListener cameraStateListener; private CameraState cameraState; private final CaptureTimeoutsWrapper captureTimeouts; + private final CameraCaptureProperties captureProps; private CameraCaptureCallback( @NonNull CameraCaptureStateListener cameraStateListener, - @NonNull CaptureTimeoutsWrapper captureTimeouts) { + @NonNull CaptureTimeoutsWrapper captureTimeouts, + @NonNull CameraCaptureProperties captureProps) { cameraState = CameraState.STATE_PREVIEW; this.cameraStateListener = cameraStateListener; this.captureTimeouts = captureTimeouts; + this.captureProps = captureProps; } /** @@ -41,8 +45,9 @@ private CameraCaptureCallback( */ public static CameraCaptureCallback create( @NonNull CameraCaptureStateListener cameraStateListener, - @NonNull CaptureTimeoutsWrapper captureTimeouts) { - return new CameraCaptureCallback(cameraStateListener, captureTimeouts); + @NonNull CaptureTimeoutsWrapper captureTimeouts, + @NonNull CameraCaptureProperties captureProps) { + return new CameraCaptureCallback(cameraStateListener, captureTimeouts, captureProps); } /** @@ -67,6 +72,16 @@ private void process(CaptureResult result) { Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + // Update capture properties + if (result instanceof TotalCaptureResult) { + Float lensAperture = result.get(CaptureResult.LENS_APERTURE); + Long sensorExposureTime = result.get(CaptureResult.SENSOR_EXPOSURE_TIME); + Integer sensorSensitivity = result.get(CaptureResult.SENSOR_SENSITIVITY); + this.captureProps.setLastLensAperture(lensAperture); + this.captureProps.setLastSensorExposureTime(sensorExposureTime); + this.captureProps.setLastSensorSensitivity(sensorSensitivity); + } + if (cameraState != CameraState.STATE_PREVIEW) { Log.d( TAG, diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java new file mode 100644 index 000000000000..68177f4ecfd6 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.types; + +public class CameraCaptureProperties { + + private Float lastLensAperture; + private Long lastSensorExposureTime; + private Integer lastSensorSensitivity; + + /** + * Gets the last known lens aperture. (As f-stop value) + * + * @return the last known lens aperture. (As f-stop value) + */ + public Float getLastLensAperture() { + return lastLensAperture; + } + + /** + * Sets the last known lens aperture. (As f-stop value) + * + * @param lastLensAperture - The last known lens aperture to set. (As f-stop value) + */ + public void setLastLensAperture(Float lastLensAperture) { + this.lastLensAperture = lastLensAperture; + } + + /** + * Gets the last known sensor exposure time in nanoseconds. + * + * @return the last known sensor exposure time in nanoseconds. + */ + public Long getLastSensorExposureTime() { + return lastSensorExposureTime; + } + + /** + * Sets the last known sensor exposure time in nanoseconds. + * + * @param lastSensorExposureTime - The last known sensor exposure time to set, in nanoseconds. + */ + public void setLastSensorExposureTime(Long lastSensorExposureTime) { + this.lastSensorExposureTime = lastSensorExposureTime; + } + + /** + * Gets the last known sensor sensitivity in ISO arithmetic units. + * + * @return the last known sensor sensitivity in ISO arithmetic units. + */ + public Integer getLastSensorSensitivity() { + return lastSensorSensitivity; + } + + /** + * Sets the last known sensor sensitivity in ISO arithmetic units. + * + * @param lastSensorSensitivity - The last known sensor sensitivity to set, in ISO arithmetic + * units. + */ + public void setLastSensorSensitivity(Integer lastSensorSensitivity) { + this.lastSensorSensitivity = lastSensorSensitivity; + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java index 4964aef8b8c9..934aff857ec7 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java @@ -17,6 +17,7 @@ import android.hardware.camera2.CaptureResult.Key; import android.hardware.camera2.TotalCaptureResult; import io.flutter.plugins.camera.CameraCaptureCallback.CameraCaptureStateListener; +import io.flutter.plugins.camera.types.CameraCaptureProperties; import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; import io.flutter.plugins.camera.types.Timeout; import io.flutter.plugins.camera.utils.TestUtils; @@ -40,6 +41,7 @@ public class CameraCaptureCallbackStatesTest extends TestCase { private CaptureRequest mockCaptureRequest; private CaptureResult mockPartialCaptureResult; private CaptureTimeoutsWrapper mockCaptureTimeouts; + private CameraCaptureProperties mockCaptureProps; private TotalCaptureResult mockTotalCaptureResult; private MockedStatic mockedStaticTimeout; private Timeout mockTimeout; @@ -83,6 +85,7 @@ protected void setUp() throws Exception { mockTotalCaptureResult = mock(TotalCaptureResult.class); mockTimeout = mock(Timeout.class); mockCaptureTimeouts = mock(CaptureTimeoutsWrapper.class); + mockCaptureProps = mock(CameraCaptureProperties.class); when(mockCaptureTimeouts.getPreCaptureFocusing()).thenReturn(mockTimeout); when(mockCaptureTimeouts.getPreCaptureMetering()).thenReturn(mockTimeout); @@ -95,7 +98,8 @@ protected void setUp() throws Exception { mockedStaticTimeout.when(() -> Timeout.create(1000)).thenReturn(mockTimeout); cameraCaptureCallback = - CameraCaptureCallback.create(mockCaptureStateListener, mockCaptureTimeouts); + CameraCaptureCallback.create( + mockCaptureStateListener, mockCaptureTimeouts, mockCaptureProps); } @Override diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java new file mode 100644 index 000000000000..75a5b25995e2 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java @@ -0,0 +1,72 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import io.flutter.plugins.camera.types.CameraCaptureProperties; +import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class CameraCaptureCallbackTest { + + private CameraCaptureCallback cameraCaptureCallback; + private CameraCaptureProperties mockCaptureProps; + + @Before + public void setUp() { + CameraCaptureCallback.CameraCaptureStateListener mockCaptureStateListener = + mock(CameraCaptureCallback.CameraCaptureStateListener.class); + CaptureTimeoutsWrapper mockCaptureTimeouts = mock(CaptureTimeoutsWrapper.class); + mockCaptureProps = mock(CameraCaptureProperties.class); + cameraCaptureCallback = + CameraCaptureCallback.create( + mockCaptureStateListener, mockCaptureTimeouts, mockCaptureProps); + } + + @Test + public void onCaptureProgressed_doesNotUpdateCameraCaptureProperties() { + CameraCaptureSession mockSession = mock(CameraCaptureSession.class); + CaptureRequest mockRequest = mock(CaptureRequest.class); + CaptureResult mockResult = mock(CaptureResult.class); + + cameraCaptureCallback.onCaptureProgressed(mockSession, mockRequest, mockResult); + + verify(mockCaptureProps, never()).setLastLensAperture(anyFloat()); + verify(mockCaptureProps, never()).setLastSensorExposureTime(anyLong()); + verify(mockCaptureProps, never()).setLastSensorSensitivity(anyInt()); + } + + @Test + public void onCaptureCompleted_updatesCameraCaptureProperties() { + CameraCaptureSession mockSession = mock(CameraCaptureSession.class); + CaptureRequest mockRequest = mock(CaptureRequest.class); + TotalCaptureResult mockResult = mock(TotalCaptureResult.class); + when(mockResult.get(CaptureResult.LENS_APERTURE)).thenReturn(1.0f); + when(mockResult.get(CaptureResult.SENSOR_EXPOSURE_TIME)).thenReturn(2L); + when(mockResult.get(CaptureResult.SENSOR_SENSITIVITY)).thenReturn(3); + + cameraCaptureCallback.onCaptureCompleted(mockSession, mockRequest, mockResult); + + verify(mockCaptureProps, times(1)).setLastLensAperture(1.0f); + verify(mockCaptureProps, times(1)).setLastSensorExposureTime(2L); + verify(mockCaptureProps, times(1)).setLastSensorSensitivity(3); + } +} diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index d88eb45945fe..ea03ce57649c 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -661,6 +661,11 @@ - (void)captureOutput:(AVCaptureOutput *)output imageBuffer[@"height"] = [NSNumber numberWithUnsignedLong:imageHeight]; imageBuffer[@"format"] = @(videoFormat); imageBuffer[@"planes"] = planes; + imageBuffer[@"lensAperture"] = [NSNumber numberWithFloat:[_captureDevice lensAperture]]; + Float64 exposureDuration = CMTimeGetSeconds([_captureDevice exposureDuration]); + Float64 nsExposureDuration = 1000000000 * exposureDuration; + imageBuffer[@"sensorExposureTime"] = [NSNumber numberWithInt:nsExposureDuration]; + imageBuffer[@"sensorSensitivity"] = [NSNumber numberWithFloat:[_captureDevice ISO]]; _imageStreamHandler.eventSink(imageBuffer); diff --git a/packages/camera/camera/lib/src/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart index 411c7e86db41..43fa763bed48 100644 --- a/packages/camera/camera/lib/src/camera_image.dart +++ b/packages/camera/camera/lib/src/camera_image.dart @@ -100,6 +100,9 @@ class CameraImage { : format = ImageFormat._fromPlatformData(data['format']), height = data['height'], width = data['width'], + lensAperture = data['lensAperture'], + sensorExposureTime = data['sensorExposureTime'], + sensorSensitivity = data['sensorSensitivity'], planes = List.unmodifiable(data['planes'] .map((dynamic planeData) => Plane._fromPlatformData(planeData))); @@ -125,4 +128,15 @@ class CameraImage { /// /// The number of planes is determined by the format of the image. final List planes; + + /// The aperture settings for this image. + /// + /// Represented as an f-stop value. + final double? lensAperture; + + /// The sensor exposure time for this image in nanoseconds. + final int? sensorExposureTime; + + /// The sensor sensitivity in standard ISO arithmetic units. + final double? sensorSensitivity; } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index a7c6a61a4ef2..08d1e3eead4f 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.0 +version: 0.9.1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/camera/camera/test/camera_image_test.dart b/packages/camera/camera/test/camera_image_test.dart index 2d827d983f3a..85d613f41485 100644 --- a/packages/camera/camera/test/camera_image_test.dart +++ b/packages/camera/camera/test/camera_image_test.dart @@ -18,6 +18,9 @@ void main() { 'format': 35, 'height': 1, 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, 'planes': [ { 'bytes': Uint8List.fromList([1, 2, 3, 4]), @@ -41,6 +44,9 @@ void main() { 'format': 875704438, 'height': 1, 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, 'planes': [ { 'bytes': Uint8List.fromList([1, 2, 3, 4]), @@ -61,6 +67,9 @@ void main() { 'format': 35, 'height': 1, 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, 'planes': [ { 'bytes': Uint8List.fromList([1, 2, 3, 4]), @@ -81,6 +90,9 @@ void main() { 'format': 1111970369, 'height': 1, 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, 'planes': [ { 'bytes': Uint8List.fromList([1, 2, 3, 4]), @@ -98,6 +110,9 @@ void main() { 'format': null, 'height': 1, 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, 'planes': [ { 'bytes': Uint8List.fromList([1, 2, 3, 4]), From 88f84104f8df94938fe67716a38d5adc6e1fd81a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 25 Aug 2021 16:39:58 -0400 Subject: [PATCH 091/123] [flutter_plugin_tools] Convert publish tests to mock git (#4263) Replaces the use of an actual git repository on the filesystem with mock git output and an in-memory filesystem. This: - makes the tests more hermetic. - simplifies the setup of some tests considerably, avoiding the need to run the command once to set up the expected state before running a second time for the intended test. - eliminates some of the special handling in the test's custom process runner (making it easier to eliminate in a PR that will follow after). Also adds some output checking in a couple of tests that didn't have enough to ensure that they were necessarily testing the right thing (e.g., testing that a specific thing didn't happen, but not checking that the publish step that could have caused that thing to happen even ran at all). --- .../tool/lib/src/publish_plugin_command.dart | 24 +- .../test/publish_plugin_command_test.dart | 563 +++++++++--------- 2 files changed, 285 insertions(+), 302 deletions(-) diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 5a75ce6af89f..be9e6d300125 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -355,11 +355,9 @@ Safe to ignore if the package is deleted in this commit. final String tag = _getTag(packageDir); print('Tagging release $tag...'); if (!getBoolArg(_dryRunFlag)) { - final io.ProcessResult result = await processRunner.run( - 'git', + final io.ProcessResult result = await (await gitDir).runCommand( ['tag', tag], - workingDir: packageDir, - logOnError: true, + throwOnError: false, ); if (result.exitCode != 0) { return false; @@ -400,11 +398,9 @@ Safe to ignore if the package is deleted in this commit. } Future _checkGitStatus(Directory packageDir) async { - final io.ProcessResult statusResult = await processRunner.run( - 'git', + final io.ProcessResult statusResult = await (await gitDir).runCommand( ['status', '--porcelain', '--ignored', packageDir.absolute.path], - workingDir: packageDir, - logOnError: true, + throwOnError: false, ); if (statusResult.exitCode != 0) { return false; @@ -421,11 +417,9 @@ Safe to ignore if the package is deleted in this commit. } Future _verifyRemote(String remote) async { - final io.ProcessResult getRemoteUrlResult = await processRunner.run( - 'git', + final io.ProcessResult getRemoteUrlResult = await (await gitDir).runCommand( ['remote', 'get-url', remote], - workingDir: packagesDir, - logOnError: true, + throwOnError: false, ); if (getRemoteUrlResult.exitCode != 0) { return null; @@ -494,11 +488,9 @@ Safe to ignore if the package is deleted in this commit. } } if (!getBoolArg(_dryRunFlag)) { - final io.ProcessResult result = await processRunner.run( - 'git', + final io.ProcessResult result = await (await gitDir).runCommand( ['push', remote.name, tag], - workingDir: packagesDir, - logOnError: true, + throwOnError: false, ); if (result.exitCode != 0) { return false; diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 576d3a4c88c8..40018b6edb61 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -8,34 +8,31 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; -import 'package:file/local.dart'; +import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/process_runner.dart'; import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; -import 'package:git/git.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; import 'package:test/test.dart'; +import 'common/plugin_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; void main() { const String testPluginName = 'foo'; - late Directory testRoot; late Directory packagesDir; late Directory pluginDir; - late GitDir gitDir; + late MockGitDir gitDir; late TestProcessRunner processRunner; + late RecordingProcessRunner gitProcessRunner; late CommandRunner commandRunner; late MockStdin mockStdin; - // This test uses a local file system instead of an in memory one throughout - // so that git actually works. In setup we initialize a mono repo of plugins - // with one package and commit everything to Git. - const FileSystem fileSystem = LocalFileSystem(); + late FileSystem fileSystem; void _createMockCredentialFile() { final String credentialPath = PublishPluginCommand.getCredentialPath(); @@ -45,20 +42,26 @@ void main() { } setUp(() async { - testRoot = fileSystem.systemTempDirectory - .createTempSync('publish_plugin_command_test-'); - // The temp directory can have symbolic links, which won't match git output; - // use a fully resolved version to avoid potential path comparison issues. - testRoot = fileSystem.directory(testRoot.resolveSymbolicLinksSync()); - packagesDir = createPackagesDirectory(parentDir: testRoot); + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + // TODO(stuartmorgan): Move this from setup to individual tests. pluginDir = createFakePlugin(testPluginName, packagesDir, examples: []); assert(pluginDir != null && pluginDir.existsSync()); - io.Process.runSync('git', ['init'], - workingDirectory: testRoot.path); - gitDir = await GitDir.fromExisting(testRoot.path); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Initial commit']); + + gitProcessRunner = RecordingProcessRunner(); + gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + final List arguments = + invocation.positionalArguments[0]! as List; + // Attach the first argument to the command to make targeting the mock + // results easier. + final String gitCommand = arguments.removeAt(0); + return gitProcessRunner.run('git-$gitCommand', arguments); + }); + processRunner = TestProcessRunner(); mockStdin = MockStdin(); commandRunner = CommandRunner('tester', '') @@ -66,10 +69,6 @@ void main() { processRunner: processRunner, stdinput: mockStdin, gitDir: gitDir)); }); - tearDown(() { - testRoot.deleteSync(recursive: true); - }); - group('Initial validation', () { test('requires a package flag', () async { Error? commandError; @@ -79,7 +78,11 @@ void main() { }); expect(commandError, isA()); - expect(output.last, contains('Must specify a package to publish.')); + expect( + output, + containsAllInOrder([ + contains('Must specify a package to publish.'), + ])); }); test('requires an existing flag', () async { @@ -91,11 +94,14 @@ void main() { }); expect(commandError, isA()); - expect(output.last, contains('iamerror does not exist')); + expect(output, + containsAllInOrder([contains('iamerror does not exist')])); }); test('refuses to proceed with dirty files', () async { - pluginDir.childFile('tmp').createSync(); + gitProcessRunner.mockProcessesForExecutable['git-status'] = [ + MockProcess(stdout: '?? ${pluginDir.childFile('tmp').path}\n') + ]; Error? commandError; final List output = await runCapturingPrint( @@ -114,7 +120,7 @@ void main() { containsAllInOrder([ contains('There are files in the package directory that haven\'t ' 'been saved in git. Refusing to publish these files:\n\n' - '?? packages/foo/tmp\n\n' + '?? /packages/foo/tmp\n\n' 'If the directory should be clean, you can run `git clean -xdf && ' 'git reset --hard HEAD` to wipe all local changes.'), contains('Failed, see above for details.'), @@ -122,20 +128,32 @@ void main() { }); test('fails immediately if the remote doesn\'t exist', () async { + gitProcessRunner.mockProcessesForExecutable['git-remote'] = [ + MockProcess(exitCode: 1), + ]; + Error? commandError; - await runCapturingPrint(commandRunner, [ - 'publish-plugin', - '--package', - testPluginName - ], errorHandler: (Error e) { + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--package', testPluginName], + errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); - expect(processRunner.results.last.stderr, contains('No such remote')); + expect( + output, + containsAllInOrder([ + contains( + 'Unable to find URL for remote upstream; cannot push tags'), + ])); }); test("doesn't validate the remote if it's not pushing tags", () async { + // Checking the remote should fail. + gitProcessRunner.mockProcessesForExecutable['git-remote'] = [ + MockProcess(exitCode: 1), + ]; + // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; @@ -148,17 +166,18 @@ void main() { '--no-tag-release' ]); - expect(output.last, 'Done!'); + expect( + output, + containsAllInOrder([ + contains('Running `pub publish ` in /packages/$testPluginName...'), + contains('Package published!'), + contains('Released [$testPluginName] successfully.'), + ])); }); test('can publish non-flutter package', () async { const String packageName = 'a_package'; createFakePackage(packageName, packagesDir); - io.Process.runSync('git', ['init'], - workingDirectory: testRoot.path); - gitDir = await GitDir.fromExisting(testRoot.path); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Initial commit']); // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; @@ -171,7 +190,15 @@ void main() { '--no-tag-release' ]); - expect(output.last, 'Done!'); + expect( + output, + containsAllInOrder( + [ + contains('Running `pub publish ` in /packages/a_package...'), + contains('Package published!'), + ], + ), + ); }); }); @@ -190,8 +217,12 @@ void main() { '--no-tag-release' ]); - expect(output, contains('Foo')); - expect(output, contains('Bar')); + expect( + output, + containsAllInOrder([ + contains('Foo'), + contains('Bar'), + ])); }); test('forwards input from the user to `pub publish`', () async { @@ -288,7 +319,10 @@ void main() { '--no-tag-release', ]); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); expect( output, containsAllInOrder([ @@ -310,10 +344,10 @@ void main() { '--no-push-tags', ]); - final String? tag = (await gitDir - .runCommand(['show-ref', '$testPluginName-v0.0.1'])) - .stdout as String?; - expect(tag, isNotEmpty); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-tag', ['$testPluginName-v0.0.1'], null))); }); test('only if publishing succeeded', () async { @@ -336,20 +370,14 @@ void main() { containsAllInOrder([ contains('Publish foo failed.'), ])); - final String? tag = (await gitDir.runCommand( - ['show-ref', '$testPluginName-v0.0.1'], - throwOnError: false)) - .stdout as String?; - expect(tag, isEmpty); + expect( + gitProcessRunner.recordedCalls, + isNot(contains( + const ProcessCall('git-tag', ['foo-v0.0.1'], null)))); }); }); group('Pushes tags', () { - setUp(() async { - await gitDir.runCommand( - ['remote', 'add', 'upstream', 'http://localhost:8000']); - }); - test('requires user confirmation', () async { processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'help'; @@ -369,7 +397,6 @@ void main() { }); test('to upstream by default', () async { - await gitDir.runCommand(['tag', 'garbage']); processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; @@ -380,15 +407,19 @@ void main() { testPluginName, ]); - expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(output.last, 'Done!'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall('git-push', + ['upstream', '$testPluginName-v0.0.1'], null))); + expect( + output, + containsAllInOrder([ + contains('Released [$testPluginName] successfully.'), + ])); }); test('does not ask for user input if the --skip-confirmation flag is on', () async { - await gitDir.runCommand(['tag', 'garbage']); processRunner.mockPublishCompleteCode = 0; _createMockCredentialFile(); @@ -400,14 +431,18 @@ void main() { testPluginName, ]); - expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(output.last, 'Done!'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall('git-push', + ['upstream', '$testPluginName-v0.0.1'], null))); + expect( + output, + containsAllInOrder([ + contains('Released [$testPluginName] successfully.'), + ])); }); test('to upstream by default, dry run', () async { - await gitDir.runCommand(['tag', 'garbage']); // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. processRunner.mockPublishCompleteCode = 1; mockStdin.readLineOutput = 'y'; @@ -415,7 +450,10 @@ void main() { final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--package', testPluginName, '--dry-run']); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); expect( output, containsAllInOrder([ @@ -428,8 +466,6 @@ void main() { }); test('to different remotes based on a flag', () async { - await gitDir.runCommand( - ['remote', 'add', 'origin', 'http://localhost:8001']); processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; @@ -442,10 +478,15 @@ void main() { 'origin', ]); - expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); - expect(processRunner.pushTagsArgs[1], 'origin'); - expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(output.last, 'Done!'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['origin', '$testPluginName-v0.0.1'], null))); + expect( + output, + containsAllInOrder([ + contains('Released [$testPluginName] successfully.'), + ])); }); test('only if tagging and pushing to remotes are both enabled', () async { @@ -459,20 +500,21 @@ void main() { '--no-tag-release', ]); - expect(processRunner.pushTagsArgs.isEmpty, isTrue); - expect(output.last, 'Done!'); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); + expect( + output, + containsAllInOrder([ + contains('Running `pub publish ` in /packages/$testPluginName...'), + contains('Package published!'), + contains('Released [$testPluginName] successfully.'), + ])); }); }); group('Auto release (all-changed flag)', () { - setUp(() async { - io.Process.runSync('git', ['init'], - workingDirectory: testRoot.path); - gitDir = await GitDir.fromExisting(testRoot.path); - await gitDir.runCommand( - ['remote', 'add', 'upstream', 'http://localhost:8000']); - }); - test('can release newly created plugins', () async { const Map httpResponsePlugin1 = { 'name': 'plugin1', @@ -511,8 +553,11 @@ void main() { 'plugin2', packagesDir.childDirectory('plugin2'), ); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; @@ -530,13 +575,14 @@ void main() { 'Packages released: plugin1, plugin2', 'Done!' ])); - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); }); test('can release newly created plugins, while there are existing plugins', @@ -578,11 +624,24 @@ void main() { ); commandRunner.addCommand(command); - // Prepare an exiting plugin and tag it + // The existing plugin. createFakePlugin('plugin0', packagesDir); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - await gitDir.runCommand(['tag', 'plugin0-v0.0.1']); + // Non-federated + final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + // federated + final Directory pluginDir2 = + createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); + + // Git results for plugin0 having been released already, and plugin1 and + // plugin2 being new. + gitProcessRunner.mockProcessesForExecutable['git-tag'] = [ + MockProcess(stdout: 'plugin0-v0.0.1\n') + ]; + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; @@ -591,19 +650,6 @@ void main() { final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); - processRunner.pushTagsArgs.clear(); - - // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); - // federated - final Directory pluginDir2 = - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - - output.addAll(await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~'])); - expect( output, containsAllInOrder([ @@ -614,13 +660,14 @@ void main() { 'Packages released: plugin1, plugin2', 'Done!' ])); - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); }); test('can release newly created plugins, dry run', () async { @@ -658,10 +705,12 @@ void main() { // federated final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. - processRunner.mockPublishCompleteCode = 1; + + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint( @@ -687,18 +736,21 @@ void main() { 'Packages released: plugin1, plugin2', 'Done!' ])); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); }); test('version change triggers releases.', () async { const Map httpResponsePlugin1 = { 'name': 'plugin1', - 'versions': [], + 'versions': ['0.0.1'], }; const Map httpResponsePlugin2 = { 'name': 'plugin2', - 'versions': [], + 'versions': ['0.0.1'], }; final MockClient mockClient = MockClient((http.Request request) async { @@ -722,57 +774,23 @@ void main() { commandRunner.addCommand(command); // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - final Directory pluginDir2 = - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); + final Directory pluginDir2 = createFakePlugin( + 'plugin2', packagesDir.childDirectory('plugin2'), + version: '0.0.2'); + + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); - - expect( - output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2', - 'Done!' - ])); - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); - - processRunner.pushTagsArgs.clear(); - - final List plugin1Pubspec = - pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); - plugin1Pubspec[plugin1Pubspec.indexWhere( - (String element) => element.contains('version:'))] = 'version: 0.0.2'; - pluginDir1 - .childFile('pubspec.yaml') - .writeAsStringSync(plugin1Pubspec.join('\n')); - final List plugin2Pubspec = - pluginDir2.childFile('pubspec.yaml').readAsLinesSync(); - plugin2Pubspec[plugin2Pubspec.indexWhere( - (String element) => element.contains('version:'))] = 'version: 0.0.2'; - pluginDir2 - .childFile('pubspec.yaml') - .writeAsStringSync(plugin2Pubspec.join('\n')); - await gitDir.runCommand(['add', '-A']); - await gitDir - .runCommand(['commit', '-m', 'Update versions to 0.0.2']); - final List output2 = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( @@ -785,14 +803,14 @@ void main() { 'Packages released: plugin1, plugin2', 'Done!' ])); - - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.2'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin2-v0.0.2'], null))); }); test( @@ -800,12 +818,12 @@ void main() { () async { const Map httpResponsePlugin1 = { 'name': 'plugin1', - 'versions': [], + 'versions': ['0.0.1'], }; const Map httpResponsePlugin2 = { 'name': 'plugin2', - 'versions': [], + 'versions': ['0.0.1'], }; final MockClient mockClient = MockClient((http.Request request) async { @@ -829,55 +847,23 @@ void main() { commandRunner.addCommand(command); // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); + pluginDir2.deleteSync(recursive: true); + + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); - expect( - output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2', - 'Done!' - ])); - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); - - processRunner.pushTagsArgs.clear(); - - final List plugin1Pubspec = - pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); - plugin1Pubspec[plugin1Pubspec.indexWhere( - (String element) => element.contains('version:'))] = 'version: 0.0.2'; - pluginDir1 - .childFile('pubspec.yaml') - .writeAsStringSync(plugin1Pubspec.join('\n')); - - pluginDir2.deleteSync(recursive: true); - - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand([ - 'commit', - '-m', - 'Update plugin1 versions to 0.0.2, delete plugin2' - ]); - final List output2 = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( @@ -890,15 +876,13 @@ void main() { 'Packages released: plugin1', 'Done!' ])); - - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs.length, 3); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); }); - test('Exiting versions do not trigger release, also prints out message.', + test('Existing versions do not trigger release, also prints out message.', () async { const Map httpResponsePlugin1 = { 'name': 'plugin1', @@ -931,17 +915,23 @@ void main() { commandRunner.addCommand(command); // Non-federated - createFakePlugin('plugin1', packagesDir, version: '0.0.2'); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2'), + final Directory pluginDir2 = createFakePlugin( + 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - await gitDir.runCommand(['tag', 'plugin1-v0.0.2']); - await gitDir.runCommand(['tag', 'plugin2-v0.0.2']); - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; - mockStdin.readLineOutput = 'y'; + + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + gitProcessRunner.mockProcessesForExecutable['git-tag'] = [ + MockProcess( + stdout: 'plugin1-v0.0.2\n' + 'plugin2-v0.0.2\n') + ]; final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); @@ -958,11 +948,14 @@ void main() { 'Done!' ])); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); }); test( - 'Exiting versions do not trigger release, but fail if the tags do not exist.', + 'Existing versions do not trigger release, but fail if the tags do not exist.', () async { const Map httpResponsePlugin1 = { 'name': 'plugin1', @@ -995,27 +988,41 @@ void main() { commandRunner.addCommand(command); // Non-federated - createFakePlugin('plugin1', packagesDir, version: '0.0.2'); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2'), + final Directory pluginDir2 = createFakePlugin( + 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; - mockStdin.readLineOutput = 'y'; + + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; Error? commandError; - await runCapturingPrint(commandRunner, [ - 'publish-plugin', - '--all-changed', - '--base-sha=HEAD~' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~'], + errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + output, + containsAllInOrder([ + contains('The version 0.0.2 of plugin1 has already been published'), + contains( + 'However, the git release tag for this version (plugin1-v0.0.2) is not found.'), + contains('The version 0.0.2 of plugin2 has already been published'), + contains( + 'However, the git release tag for this version (plugin2-v0.0.2) is not found.'), + ])); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); }); test('No version change does not release any plugins', () async { @@ -1025,20 +1032,11 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - io.Process.runSync('git', ['init'], - workingDirectory: testRoot.path); - gitDir = await GitDir.fromExisting(testRoot.path); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - - pluginDir1.childFile('plugin1.dart').createSync(); - pluginDir2.childFile('plugin2.dart').createSync(); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add dart files']); - - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; - mockStdin.readLineOutput = 'y'; + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('plugin1.dart').path}\n' + '${pluginDir2.childFile('plugin2.dart').path}\n') + ]; final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); @@ -1051,7 +1049,10 @@ void main() { 'No version updates in this commit.', 'Done!' ])); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); }); test('Do not release flutter_plugin_tools', () async { @@ -1080,11 +1081,9 @@ void main() { final Directory flutterPluginTools = createFakePlugin('flutter_plugin_tools', packagesDir); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; - mockStdin.readLineOutput = 'y'; + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: flutterPluginTools.childFile('pubspec.yaml').path) + ]; final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); @@ -1101,19 +1100,18 @@ void main() { 'Running `pub publish ` in ${flutterPluginTools.path}...\n', ), isFalse); - expect(processRunner.pushTagsArgs, isEmpty); - processRunner.pushTagsArgs.clear(); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); }); }); } class TestProcessRunner extends ProcessRunner { - final List results = []; // Most recent returned publish process. late MockProcess mockPublishProcess; final List mockPublishArgs = []; - final MockProcessResult mockPushTagsResult = MockProcessResult(); - final List pushTagsArgs = []; String? mockPublishStdout; String? mockPublishStderr; @@ -1129,15 +1127,8 @@ class TestProcessRunner extends ProcessRunner { Encoding stdoutEncoding = io.systemEncoding, Encoding stderrEncoding = io.systemEncoding, }) async { - // Don't ever really push tags. - if (executable == 'git' && args.isNotEmpty && args[0] == 'push') { - pushTagsArgs.addAll(args); - return mockPushTagsResult; - } - final io.ProcessResult result = io.Process.runSync(executable, args, workingDirectory: workingDir?.path); - results.add(result); if (result.exitCode != 0) { throw ToolExit(result.exitCode); } From 56f092a8105d2a21d26844edb1bf8458f79f195e Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 26 Aug 2021 02:36:07 +0200 Subject: [PATCH 092/123] [camera_web] Add `onCameraClosing` implementation (#4259) --- .../example/integration_test/camera_test.dart | 74 ++++++- .../integration_test/camera_web_test.dart | 195 ++++++++++++------ .../camera/camera_web/lib/src/camera.dart | 43 +++- .../camera/camera_web/lib/src/camera_web.dart | 18 +- 4 files changed, 267 insertions(+), 63 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 1d1659352f26..f331cc1485ab 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -5,6 +5,7 @@ import 'dart:html'; import 'dart:ui'; +import 'package:async/async.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_service.dart'; @@ -843,10 +844,81 @@ void main() { await camera.initialize(); - camera.dispose(); + await camera.dispose(); expect(camera.videoElement.srcObject, isNull); }); }); + + group('events', () { + group('onEnded', () { + testWidgets( + 'emits the default video track ' + 'when it emits an ended event', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + final streamQueue = StreamQueue(camera.onEnded); + + await camera.initialize(); + + final videoTracks = camera.stream!.getVideoTracks(); + final defaultVideoTrack = videoTracks.first; + + defaultVideoTrack.dispatchEvent(Event('ended')); + + expect( + await streamQueue.next, + equals(defaultVideoTrack), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits the default video track ' + 'when the camera is stopped', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + final streamQueue = StreamQueue(camera.onEnded); + + await camera.initialize(); + + final videoTracks = camera.stream!.getVideoTracks(); + final defaultVideoTrack = videoTracks.first; + + camera.stop(); + + expect( + await streamQueue.next, + equals(defaultVideoTrack), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'no longer emits the default video track ' + 'when the camera is disposed', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + await camera.dispose(); + + expect( + camera.onEndedStreamController.isClosed, + isTrue, + ); + }); + }); + }); }); } diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index d48df122277f..9ab8c511f753 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -563,24 +563,34 @@ void main() { late Camera camera; late VideoElement videoElement; + late StreamController errorStreamController, abortStreamController; + late StreamController endedStreamController; + setUp(() { camera = MockCamera(); videoElement = MockVideoElement(); - when(() => camera.videoElement).thenReturn(videoElement); - when(() => videoElement.onError) - .thenAnswer((_) => FakeElementStream(Stream.empty())); - when(() => videoElement.onAbort) - .thenAnswer((_) => FakeElementStream(Stream.empty())); - }); + errorStreamController = StreamController(); + abortStreamController = StreamController(); + endedStreamController = StreamController(); - testWidgets('initializes and plays the camera', (tester) async { when(camera.getVideoSize).thenAnswer( (_) => Future.value(Size(10, 10)), ); when(camera.initialize).thenAnswer((_) => Future.value()); when(camera.play).thenAnswer((_) => Future.value()); + when(() => camera.videoElement).thenReturn(videoElement); + when(() => videoElement.onError) + .thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when(() => videoElement.onAbort) + .thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + when(() => camera.onEnded) + .thenAnswer((_) => endedStreamController.stream); + }); + + testWidgets('initializes and plays the camera', (tester) async { // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -590,6 +600,32 @@ void main() { verify(camera.play).called(1); }); + testWidgets('starts listening to the camera video error and abort events', + (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect(errorStreamController.hasListener, isFalse); + expect(abortStreamController.hasListener, isFalse); + + await CameraPlatform.instance.initializeCamera(cameraId); + + expect(errorStreamController.hasListener, isTrue); + expect(abortStreamController.hasListener, isTrue); + }); + + testWidgets('starts listening to the camera ended events', + (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect(endedStreamController.hasListener, isFalse); + + await CameraPlatform.instance.initializeCamera(cameraId); + + expect(endedStreamController.hasListener, isTrue); + }); + group('throws PlatformException', () { testWidgets( 'with notFound error ' @@ -1610,6 +1646,37 @@ void main() { }); group('dispose', () { + late Camera camera; + late VideoElement videoElement; + + late StreamController errorStreamController, abortStreamController; + late StreamController endedStreamController; + + setUp(() { + camera = MockCamera(); + videoElement = MockVideoElement(); + + errorStreamController = StreamController(); + abortStreamController = StreamController(); + endedStreamController = StreamController(); + + when(camera.getVideoSize).thenAnswer( + (_) => Future.value(Size(10, 10)), + ); + when(camera.initialize).thenAnswer((_) => Future.value()); + when(camera.play).thenAnswer((_) => Future.value()); + when(camera.dispose).thenAnswer((_) => Future.value()); + + when(() => camera.videoElement).thenReturn(videoElement); + when(() => videoElement.onError) + .thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when(() => videoElement.onAbort) + .thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + when(() => camera.onEnded) + .thenAnswer((_) => endedStreamController.stream); + }); + testWidgets('disposes the correct camera', (tester) async { const firstCameraId = 0; const secondCameraId = 1; @@ -1642,38 +1709,26 @@ void main() { ); }); - testWidgets('cancels camera video and abort error subscriptions', + testWidgets('cancels the camera video error and abort subscriptions', (tester) async { - final camera = MockCamera(); - final videoElement = MockVideoElement(); - - final errorStreamController = StreamController(); - final abortStreamController = StreamController(); + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - when(camera.getVideoSize).thenAnswer( - (_) => Future.value(Size(10, 10)), - ); - when(camera.initialize).thenAnswer((_) => Future.value()); - when(camera.play).thenAnswer((_) => Future.value()); + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.dispose(cameraId); - when(() => camera.videoElement).thenReturn(videoElement); - when(() => videoElement.onError) - .thenAnswer((_) => FakeElementStream(errorStreamController.stream)); - when(() => videoElement.onAbort) - .thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + expect(errorStreamController.hasListener, isFalse); + expect(abortStreamController.hasListener, isFalse); + }); + testWidgets('cancels the camera ended subscriptions', (tester) async { // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; await CameraPlatform.instance.initializeCamera(cameraId); - - expect(errorStreamController.hasListener, isTrue); - expect(abortStreamController.hasListener, isTrue); - await CameraPlatform.instance.dispose(cameraId); - expect(errorStreamController.hasListener, isFalse); - expect(abortStreamController.hasListener, isFalse); + expect(endedStreamController.hasListener, isFalse); }); group('throws PlatformException', () { @@ -1749,6 +1804,36 @@ void main() { }); group('events', () { + late Camera camera; + late VideoElement videoElement; + + late StreamController errorStreamController, abortStreamController; + late StreamController endedStreamController; + + setUp(() { + camera = MockCamera(); + videoElement = MockVideoElement(); + + errorStreamController = StreamController(); + abortStreamController = StreamController(); + endedStreamController = StreamController(); + + when(camera.getVideoSize).thenAnswer( + (_) => Future.value(Size(10, 10)), + ); + when(camera.initialize).thenAnswer((_) => Future.value()); + when(camera.play).thenAnswer((_) => Future.value()); + + when(() => camera.videoElement).thenReturn(videoElement); + when(() => videoElement.onError) + .thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when(() => videoElement.onAbort) + .thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + when(() => camera.onEnded) + .thenAnswer((_) => endedStreamController.stream); + }); + testWidgets( 'onCameraInitialized emits a CameraInitializedEvent ' 'on initializeCamera', (tester) async { @@ -1805,46 +1890,40 @@ void main() { ); }); - testWidgets('onCameraClosing throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.onCameraClosing(cameraId), - throwsUnimplementedError, - ); - }); + testWidgets( + 'onCameraClosing emits a CameraClosingEvent ' + 'on the camera ended event', (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - group('onCameraError', () { - late Camera camera; - late VideoElement videoElement; + final Stream eventStream = + CameraPlatform.instance.onCameraClosing(cameraId); - late StreamController errorStreamController, - abortStreamController; + final streamQueue = StreamQueue(eventStream); - setUp(() { - camera = MockCamera(); - videoElement = MockVideoElement(); + await CameraPlatform.instance.initializeCamera(cameraId); - errorStreamController = StreamController(); - abortStreamController = StreamController(); + endedStreamController.add(MockMediaStreamTrack()); - when(camera.getVideoSize).thenAnswer( - (_) => Future.value(Size(10, 10)), - ); - when(camera.initialize).thenAnswer((_) => Future.value()); - when(camera.play).thenAnswer((_) => Future.value()); + expect( + await streamQueue.next, + equals( + CameraClosingEvent(cameraId), + ), + ); - when(() => camera.videoElement).thenReturn(videoElement); - when(() => videoElement.onError).thenAnswer( - (_) => FakeElementStream(errorStreamController.stream)); - when(() => videoElement.onAbort).thenAnswer( - (_) => FakeElementStream(abortStreamController.stream)); + await streamQueue.cancel(); + }); + group('onCameraError', () { + setUp(() { // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; }); testWidgets( 'emits a CameraErrorEvent ' - 'on initialize video error ' + 'on the camera video error event ' 'with a message', (tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); @@ -1879,7 +1958,7 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on initialize video error ' + 'on the camera video error event ' 'with no message', (tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); @@ -1910,7 +1989,7 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on initialize abort error', (tester) async { + 'on the camera video abort event', (tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index c1343ceccf49..74d8546fbb12 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:html' as html; import 'dart:ui'; @@ -67,6 +68,23 @@ class Camera { /// Initialized in [initialize] and [play], reset in [stop]. html.MediaStream? stream; + /// The stream of the camera video tracks that have ended playing. + /// + /// This occurs when there is no more camera stream data, e.g. + /// the user has stopped the stream by changing the camera device, + /// revoked the camera permissions or ejected the camera device. + /// + /// MediaStreamTrack.onended: + /// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/onended + Stream get onEnded => onEndedStreamController.stream; + + /// The stream controller for the [onEnded] stream. + @visibleForTesting + final onEndedStreamController = + StreamController.broadcast(); + + StreamSubscription? _onEndedSubscription; + /// The camera flash mode. @visibleForTesting FlashMode? flashMode; @@ -80,6 +98,7 @@ class Camera { /// Initializes the camera stream displayed in the [videoElement]. /// Registers the camera view with [textureId] under [_getViewType] type. + /// Emits the camera default video track on the [onEnded] stream when it ends. Future initialize() async { stream = await _cameraService.getMediaStreamForOptions( options, @@ -103,6 +122,16 @@ class Camera { ..muted = !options.audio.enabled ..srcObject = stream ..setAttribute('playsinline', ''); + + final videoTracks = stream!.getVideoTracks(); + + if (videoTracks.isNotEmpty) { + final defaultVideoTrack = videoTracks.first; + + _onEndedSubscription = defaultVideoTrack.onEnded.listen((html.Event _) { + onEndedStreamController.add(defaultVideoTrack); + }); + } } /// Starts the camera stream. @@ -126,7 +155,12 @@ class Camera { /// Stops the camera stream and resets the camera source. void stop() { - final tracks = videoElement.srcObject?.getTracks(); + final videoTracks = stream!.getVideoTracks(); + if (videoTracks.isNotEmpty) { + onEndedStreamController.add(videoTracks.first); + } + + final tracks = stream?.getTracks(); if (tracks != null) { for (final track in tracks) { track.stop(); @@ -303,7 +337,7 @@ class Camera { /// Disposes the camera by stopping the camera stream /// and reloading the camera source. - void dispose() { + Future dispose() async { /// Stop the camera stream. stop(); @@ -311,6 +345,11 @@ class Camera { videoElement ..srcObject = null ..load(); + + await _onEndedSubscription?.cancel(); + _onEndedSubscription = null; + + await onEndedStreamController.close(); } /// Applies default styles to the video [element]. diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 8b131f5d4f6e..19ee43f36660 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -61,6 +61,9 @@ class CameraPlugin extends CameraPlatform { final _cameraVideoAbortSubscriptions = >{}; + final _cameraEndedSubscriptions = + >{}; + /// Returns a stream of camera events for the given [cameraId]. Stream _cameraEvents(int cameraId) => cameraEventStreamController.stream @@ -273,6 +276,15 @@ class CameraPlugin extends CameraPlatform { await camera.play(); + // Add camera's closing events to the camera events stream. + // The onEnded stream fires when there is no more camera stream data. + _cameraEndedSubscriptions[cameraId] = + camera.onEnded.listen((html.MediaStreamTrack _) { + cameraEventStreamController.add( + CameraClosingEvent(cameraId), + ); + }); + final cameraSize = await camera.getVideoSize(); cameraEventStreamController.add( @@ -313,7 +325,7 @@ class CameraPlugin extends CameraPlatform { @override Stream onCameraClosing(int cameraId) { - throw UnimplementedError('onCameraClosing() is not implemented.'); + return _cameraEvents(cameraId).whereType(); } @override @@ -548,13 +560,15 @@ class CameraPlugin extends CameraPlatform { @override Future dispose(int cameraId) async { try { - getCamera(cameraId).dispose(); + await getCamera(cameraId).dispose(); await _cameraVideoErrorSubscriptions[cameraId]?.cancel(); await _cameraVideoAbortSubscriptions[cameraId]?.cancel(); + await _cameraEndedSubscriptions[cameraId]?.cancel(); cameras.remove(cameraId); _cameraVideoErrorSubscriptions.remove(cameraId); _cameraVideoAbortSubscriptions.remove(cameraId); + _cameraEndedSubscriptions.remove(cameraId); } on html.DomException catch (e) { throw PlatformException(code: e.name, message: e.message); } From 79595de6752d75e226d601e275776e709e343f69 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 26 Aug 2021 09:10:15 -0400 Subject: [PATCH 093/123] [flutter_plugin_tool] Fix CHANGELOG validation failure summary (#4266) The error summary for a CHANGELOG validation failure was written when the only thing being checked was that the versions matched, but now there are other ways to fail as well (i.e., leaving NEXT). This fixes the summary message to be more generic so that it doesn't mislead people who hit validation failures. While adding the test for this message, I discovered that almost all of the tests were actually talking to pub.dev, causing their behavior to in some cases depend on whether a package with that name happened to have been published, and if so what its version was. In order to make the tests hermetic and predictable, this fixes that by making all tests use a mock HTTP client. --- .../tool/lib/src/version_check_command.dart | 2 +- .../tool/test/version_check_command_test.dart | 66 +++++++------------ 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 67a81b967a8e..6b49c40d66bb 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -178,7 +178,7 @@ class VersionCheckCommand extends PackageLoopingCommand { if (!(await _validateChangelogVersion(package, pubspec: pubspec, pubspecVersionChanged: versionChanged))) { - errors.add('pubspec.yaml and CHANGELOG.md have different versions'); + errors.add('CHANGELOG.md failed validation.'); } return errors.isEmpty diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 7765073feb08..9ab7c57089a3 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -54,11 +54,15 @@ void main() { late List> gitDirCommands; Map gitShowResponses; late MockGitDir gitDir; + // Ignored if mockHttpResponse is set. + int mockHttpStatus; + Map? mockHttpResponse; setUp(() { fileSystem = MemoryFileSystem(); mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); + gitDirCommands = >[]; gitShowResponses = {}; gitDir = MockGitDir(); @@ -81,9 +85,21 @@ void main() { } return Future.value(mockProcessResult); }); + + // Default to simulating the plugin never having been published. + mockHttpStatus = 404; + mockHttpResponse = null; + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response(json.encode(mockHttpResponse), + mockHttpResponse == null ? mockHttpStatus : 200); + }); + processRunner = RecordingProcessRunner(); final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, platform: mockPlatform, gitDir: gitDir); + processRunner: processRunner, + platform: mockPlatform, + gitDir: gitDir, + httpClient: mockClient); runner = CommandRunner( 'version_check_command', 'Test for $VersionCheckCommand'); @@ -456,7 +472,9 @@ void main() { output, containsAllInOrder([ contains('When bumping the version for release, the NEXT section ' - 'should be incorporated into the new version\'s release notes.') + 'should be incorporated into the new version\'s release notes.'), + contains('plugin:\n' + ' CHANGELOG.md failed validation.'), ]), ); }); @@ -497,7 +515,7 @@ void main() { }); test('allows valid against pub', () async { - const Map httpResponse = { + mockHttpResponse = { 'name': 'some_package', 'versions': [ '0.0.1', @@ -505,15 +523,6 @@ void main() { '1.0.0', ], }; - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response(json.encode(httpResponse), 200); - }); - final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { @@ -531,22 +540,13 @@ void main() { }); test('denies invalid against pub', () async { - const Map httpResponse = { + mockHttpResponse = { 'name': 'some_package', 'versions': [ '0.0.1', '0.0.2', ], }; - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response(json.encode(httpResponse), 200); - }); - final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { @@ -578,15 +578,7 @@ ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: N test( 'throw and print error message if http request failed when checking against pub', () async { - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response('xx', 400); - }); - final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); + mockHttpStatus = 400; createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { @@ -609,7 +601,7 @@ ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: N contains(''' ${indentation}Error fetching version on pub for plugin. ${indentation}HTTP Status 400 -${indentation}HTTP response: xx +${indentation}HTTP response: null ''') ]), ); @@ -617,15 +609,7 @@ ${indentation}HTTP response: xx test('when checking against pub, allow any version if http status is 404.', () async { - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response('xx', 404); - }); - final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); + mockHttpStatus = 404; createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { From 9cea8db971ac0d0240736d2358a6ade0d9950652 Mon Sep 17 00:00:00 2001 From: Casey Hillers Date: Thu, 26 Aug 2021 10:26:06 -0700 Subject: [PATCH 094/123] [ci.yaml] Add auto-roller (#4270) --- .ci.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.ci.yaml b/.ci.yaml index c2b7deebab14..86bc72c7aebf 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -44,3 +44,11 @@ targets: {"dependency": "vs_build"} ] scheduler: luci + + - name: Linux ci_yaml plugins roller + recipe: infra/ci_yaml + bringup: true + timeout: 30 + scheduler: luci + runIf: + - .ci.yaml From ee8355bdcff82cd2ab887141a0eb56fb5a71bc64 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 26 Aug 2021 15:01:06 -0400 Subject: [PATCH 095/123] [flutter_plugin_tools] Check 'implements' for unpublished plugins (#4273) --- packages/camera/camera_web/pubspec.yaml | 1 + .../tool/lib/src/pubspec_check_command.dart | 19 +++--- .../tool/test/pubspec_check_command_test.dart | 64 +++++++++++++++++++ 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index 822af60a979b..70194d9037d4 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -15,6 +15,7 @@ environment: flutter: plugin: + implements: camera platforms: web: pluginClass: CameraPlugin diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index def2adaf2788..29f9ea733a03 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -97,6 +97,16 @@ class PubspecCheckCommand extends PackageLoopingCommand { printError('$listIndentation${sectionOrder.join('\n$listIndentation')}'); } + if (isPlugin) { + final String? error = _checkForImplementsError(pubspec, package: package); + if (error != null) { + printError('$indentation$error'); + passing = false; + } + } + + // Ignore metadata that's only relevant for published packages if the + // packages is not intended for publishing. if (pubspec.publishTo != 'none') { final List repositoryErrors = _checkForRepositoryLinkErrors(pubspec, package: package); @@ -114,15 +124,6 @@ class PubspecCheckCommand extends PackageLoopingCommand { '${indentation * 2}$_expectedIssueLinkFormat'); passing = false; } - - if (isPlugin) { - final String? error = - _checkForImplementsError(pubspec, package: package); - if (error != null) { - printError('$indentation$error'); - passing = false; - } - } } return passing; diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 833f7b601e50..c5d36013c40b 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -55,6 +55,7 @@ void main() { String? repositoryPackagesDirRelativePath, bool includeHomepage = false, bool includeIssueTracker = true, + bool publishable = true, }) { final String repositoryPath = repositoryPackagesDirRelativePath ?? name; final String repoLink = 'https://github.com/flutter/' @@ -69,6 +70,7 @@ ${includeRepository ? 'repository: $repoLink' : ''} ${includeHomepage ? 'homepage: $repoLink' : ''} ${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''} version: 1.0.0 +${publishable ? '' : 'publish_to: \'none\''} '''; } @@ -567,5 +569,67 @@ ${devDependenciesSection()} ]), ); }); + + test('validates some properties even for unpublished packages', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + // Environment section is in the wrong location. + // Missing 'implements'. + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_foo', isPlugin: true, publishable: false)} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +${environmentSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + contains('Missing "implements: plugin_a" in "plugin" section.'), + ]), + ); + }); + + test('ignores some checks for unpublished packages', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + // Missing metadata that is only useful for published packages, such as + // repository and issue tracker. + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection( + 'plugin', + isPlugin: true, + publishable: false, + includeRepository: false, + includeIssueTracker: false, + )} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin...'), + contains('No issues found!'), + ]), + ); + }); }); } From 00ace648e5b6116f2b7dcc7a2f14c3a299ab1c69 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 26 Aug 2021 15:05:28 -0400 Subject: [PATCH 096/123] [flutter_plugin_tools] Move publish tests to RecordingProcessRunner (#4269) Replaces almost all of the `TestProcessRunner`, which was specific to the `publish` tests, with the repo-standard `RecordingProcessRunner` (which now has most of the capabilities these tests need). This finishes aligning these tests with the rest of the repository tests, so they will be easier to maintain as part of the overall repository. To support this, `RecordingProcessRunner` was modified slightly to return a succeeding, no-output process by default for `start`. That makes it consistent with its existing `run` behavior, so is a good change in general. --- .../tool/test/publish_check_command_test.dart | 17 - .../test/publish_plugin_command_test.dart | 333 ++++++++---------- script/tool/test/util.dart | 5 +- 3 files changed, 152 insertions(+), 203 deletions(-) diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index 65b0cb54547c..e1ab0e224e44 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -49,11 +49,6 @@ void main() { final Directory plugin2Dir = createFakePlugin('plugin_tools_test_package_b', packagesDir); - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(), - MockProcess(), - ]; - await runCapturingPrint(runner, ['publish-check']); expect( @@ -87,10 +82,6 @@ void main() { final Directory dir = createFakePlugin('c', packagesDir); await dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(), - ]; - expect(() => runCapturingPrint(runner, ['publish-check']), throwsA(isA())); }); @@ -245,10 +236,6 @@ void main() { createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(), - ]; - final List output = await runCapturingPrint( runner, ['publish-check', '--machine']); @@ -318,10 +305,6 @@ void main() { await plugin1Dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(), - ]; - bool hasError = false; final List output = await runCapturingPrint( runner, ['publish-check', '--machine'], diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 40018b6edb61..663c2633a9db 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -10,7 +10,6 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/process_runner.dart'; import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; @@ -23,13 +22,11 @@ import 'mocks.dart'; import 'util.dart'; void main() { - const String testPluginName = 'foo'; + final String flutterCommand = getFlutterCommand(const LocalPlatform()); late Directory packagesDir; - late Directory pluginDir; late MockGitDir gitDir; late TestProcessRunner processRunner; - late RecordingProcessRunner gitProcessRunner; late CommandRunner commandRunner; late MockStdin mockStdin; late FileSystem fileSystem; @@ -44,25 +41,21 @@ void main() { setUp(() async { fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); - // TODO(stuartmorgan): Move this from setup to individual tests. - pluginDir = - createFakePlugin(testPluginName, packagesDir, examples: []); - assert(pluginDir != null && pluginDir.existsSync()); - gitProcessRunner = RecordingProcessRunner(); + processRunner = TestProcessRunner(); gitDir = MockGitDir(); when(gitDir.path).thenReturn(packagesDir.parent.path); when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) .thenAnswer((Invocation invocation) { final List arguments = invocation.positionalArguments[0]! as List; - // Attach the first argument to the command to make targeting the mock - // results easier. + // Route git calls through the process runner, to make mock output + // consistent with outer processes. Attach the first argument to the + // command to make targeting the mock results easier. final String gitCommand = arguments.removeAt(0); - return gitProcessRunner.run('git-$gitCommand', arguments); + return processRunner.run('git-$gitCommand', arguments); }); - processRunner = TestProcessRunner(); mockStdin = MockStdin(); commandRunner = CommandRunner('tester', '') ..addCommand(PublishPluginCommand(packagesDir, @@ -99,18 +92,17 @@ void main() { }); test('refuses to proceed with dirty files', () async { - gitProcessRunner.mockProcessesForExecutable['git-status'] = [ + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable['git-status'] = [ MockProcess(stdout: '?? ${pluginDir.childFile('tmp').path}\n') ]; Error? commandError; - final List output = await runCapturingPrint( - commandRunner, [ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--package', 'foo', '--no-push-tags'], + errorHandler: (Error e) { commandError = e; }); @@ -128,13 +120,15 @@ void main() { }); test('fails immediately if the remote doesn\'t exist', () async { - gitProcessRunner.mockProcessesForExecutable['git-remote'] = [ + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable['git-remote'] = [ MockProcess(exitCode: 1), ]; Error? commandError; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--package', testPluginName], + final List output = await runCapturingPrint( + commandRunner, ['publish-plugin', '--package', 'foo'], errorHandler: (Error e) { commandError = e; }); @@ -149,19 +143,18 @@ void main() { }); test("doesn't validate the remote if it's not pushing tags", () async { + createFakePlugin('foo', packagesDir, examples: []); + // Checking the remote should fail. - gitProcessRunner.mockProcessesForExecutable['git-remote'] = [ + processRunner.mockProcessesForExecutable['git-remote'] = [ MockProcess(exitCode: 1), ]; - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; - - final List output = - await runCapturingPrint(commandRunner, [ + final List output = await runCapturingPrint( + commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release' ]); @@ -169,17 +162,15 @@ void main() { expect( output, containsAllInOrder([ - contains('Running `pub publish ` in /packages/$testPluginName...'), + contains('Running `pub publish ` in /packages/foo...'), contains('Package published!'), - contains('Released [$testPluginName] successfully.'), + contains('Released [foo] successfully.'), ])); }); test('can publish non-flutter package', () async { const String packageName = 'a_package'; createFakePackage(packageName, packagesDir); - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; final List output = await runCapturingPrint( commandRunner, [ @@ -204,15 +195,21 @@ void main() { group('Publishes package', () { test('while showing all output from pub publish to the user', () async { - processRunner.mockPublishStdout = 'Foo'; - processRunner.mockPublishStderr = 'Bar'; - processRunner.mockPublishCompleteCode = 0; + createFakePlugin('foo', packagesDir, examples: []); - final List output = - await runCapturingPrint(commandRunner, [ + processRunner.mockProcessesForExecutable[flutterCommand] = [ + MockProcess( + stdout: 'Foo', + stderr: 'Bar', + stdoutEncoding: utf8, + stderrEncoding: utf8) // pub publish + ]; + + final List output = await runCapturingPrint( + commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release' ]); @@ -226,13 +223,14 @@ void main() { }); test('forwards input from the user to `pub publish`', () async { + createFakePlugin('foo', packagesDir, examples: []); + mockStdin.mockUserInputs.add(utf8.encode('user input')); - processRunner.mockPublishCompleteCode = 0; await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release' ]); @@ -242,35 +240,38 @@ void main() { }); test('forwards --pub-publish-flags to pub publish', () async { - processRunner.mockPublishCompleteCode = 0; + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release', '--pub-publish-flags', '--dry-run,--server=foo' ]); - expect(processRunner.mockPublishArgs.length, 4); - expect(processRunner.mockPublishArgs[0], 'pub'); - expect(processRunner.mockPublishArgs[1], 'publish'); - expect(processRunner.mockPublishArgs[2], '--dry-run'); - expect(processRunner.mockPublishArgs[3], '--server=foo'); + expect( + processRunner.recordedCalls, + contains(ProcessCall( + flutterCommand, + const ['pub', 'publish', '--dry-run', '--server=foo'], + pluginDir.path))); }); test( '--skip-confirmation flag automatically adds --force to --pub-publish-flags', () async { - processRunner.mockPublishCompleteCode = 0; _createMockCredentialFile(); + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release', '--skip-confirmation', @@ -278,22 +279,27 @@ void main() { '--server=foo' ]); - expect(processRunner.mockPublishArgs.length, 4); - expect(processRunner.mockPublishArgs[0], 'pub'); - expect(processRunner.mockPublishArgs[1], 'publish'); - expect(processRunner.mockPublishArgs[2], '--server=foo'); - expect(processRunner.mockPublishArgs[3], '--force'); + expect( + processRunner.recordedCalls, + contains(ProcessCall( + flutterCommand, + const ['pub', 'publish', '--server=foo', '--force'], + pluginDir.path))); }); test('throws if pub publish fails', () async { - processRunner.mockPublishCompleteCode = 128; + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable[flutterCommand] = [ + MockProcess(exitCode: 128) // pub publish + ]; Error? commandError; final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release', ], errorHandler: (Error e) { @@ -309,18 +315,21 @@ void main() { }); test('publish, dry run', () async { + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); + final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--dry-run', '--no-push-tags', '--no-tag-release', ]); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); expect( @@ -335,30 +344,31 @@ void main() { group('Tags release', () { test('with the version and name from the pubspec.yaml', () async { - processRunner.mockPublishCompleteCode = 0; - + createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', ]); - expect( - gitProcessRunner.recordedCalls, - contains(const ProcessCall( - 'git-tag', ['$testPluginName-v0.0.1'], null))); + expect(processRunner.recordedCalls, + contains(const ProcessCall('git-tag', ['foo-v0.0.1'], null))); }); test('only if publishing succeeded', () async { - processRunner.mockPublishCompleteCode = 128; + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable[flutterCommand] = [ + MockProcess(exitCode: 128) // pub publish + ]; Error? commandError; final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', ], errorHandler: (Error e) { commandError = e; @@ -371,7 +381,7 @@ void main() { contains('Publish foo failed.'), ])); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, isNot(contains( const ProcessCall('git-tag', ['foo-v0.0.1'], null)))); }); @@ -379,7 +389,8 @@ void main() { group('Pushes tags', () { test('requires user confirmation', () async { - processRunner.mockPublishCompleteCode = 0; + createFakePlugin('foo', packagesDir, examples: []); + mockStdin.readLineOutput = 'help'; Error? commandError; @@ -387,7 +398,7 @@ void main() { await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', ], errorHandler: (Error e) { commandError = e; }); @@ -397,61 +408,63 @@ void main() { }); test('to upstream by default', () async { - processRunner.mockPublishCompleteCode = 0; + createFakePlugin('foo', packagesDir, examples: []); + mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', ]); expect( - gitProcessRunner.recordedCalls, - contains(const ProcessCall('git-push', - ['upstream', '$testPluginName-v0.0.1'], null))); + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'foo-v0.0.1'], null))); expect( output, containsAllInOrder([ - contains('Released [$testPluginName] successfully.'), + contains('Released [foo] successfully.'), ])); }); test('does not ask for user input if the --skip-confirmation flag is on', () async { - processRunner.mockPublishCompleteCode = 0; _createMockCredentialFile(); + createFakePlugin('foo', packagesDir, examples: []); final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--skip-confirmation', '--package', - testPluginName, + 'foo', ]); expect( - gitProcessRunner.recordedCalls, - contains(const ProcessCall('git-push', - ['upstream', '$testPluginName-v0.0.1'], null))); + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'foo-v0.0.1'], null))); expect( output, containsAllInOrder([ - contains('Released [$testPluginName] successfully.'), + contains('Released [foo] successfully.'), ])); }); test('to upstream by default, dry run', () async { - // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. - processRunner.mockPublishCompleteCode = 1; + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); + mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--package', testPluginName, '--dry-run']); + ['publish-plugin', '--package', 'foo', '--dry-run']); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); expect( @@ -459,57 +472,58 @@ void main() { containsAllInOrder([ '=============== DRY RUN ===============', 'Running `pub publish ` in ${pluginDir.path}...\n', - 'Tagging release $testPluginName-v0.0.1...', + 'Tagging release foo-v0.0.1...', 'Pushing tag to upstream...', 'Done!' ])); }); test('to different remotes based on a flag', () async { - processRunner.mockPublishCompleteCode = 0; + createFakePlugin('foo', packagesDir, examples: []); + mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--remote', 'origin', ]); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( - 'git-push', ['origin', '$testPluginName-v0.0.1'], null))); + 'git-push', ['origin', 'foo-v0.0.1'], null))); expect( output, containsAllInOrder([ - contains('Released [$testPluginName] successfully.'), + contains('Released [foo] successfully.'), ])); }); test('only if tagging and pushing to remotes are both enabled', () async { - processRunner.mockPublishCompleteCode = 0; + createFakePlugin('foo', packagesDir, examples: []); final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-tag-release', ]); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); expect( output, containsAllInOrder([ - contains('Running `pub publish ` in /packages/$testPluginName...'), + contains('Running `pub publish ` in /packages/foo...'), contains('Package published!'), - contains('Released [$testPluginName] successfully.'), + contains('Released [foo] successfully.'), ])); }); }); @@ -553,13 +567,11 @@ void main() { 'plugin2', packagesDir.childDirectory('plugin2'), ); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') ]; - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, @@ -576,11 +588,11 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); }); @@ -634,17 +646,15 @@ void main() { // Git results for plugin0 having been released already, and plugin1 and // plugin2 being new. - gitProcessRunner.mockProcessesForExecutable['git-tag'] = [ + processRunner.mockProcessesForExecutable['git-tag'] = [ MockProcess(stdout: 'plugin0-v0.0.1\n') ]; - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') ]; - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, @@ -661,11 +671,11 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); }); @@ -706,7 +716,7 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') @@ -737,7 +747,7 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); @@ -781,14 +791,12 @@ void main() { 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') ]; - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; final List output2 = await runCapturingPrint(commandRunner, @@ -804,11 +812,11 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin2-v0.0.2'], null))); }); @@ -854,14 +862,12 @@ void main() { createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); pluginDir2.deleteSync(recursive: true); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') ]; - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; final List output2 = await runCapturingPrint(commandRunner, @@ -877,7 +883,7 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); }); @@ -922,12 +928,12 @@ void main() { 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') ]; - gitProcessRunner.mockProcessesForExecutable['git-tag'] = [ + processRunner.mockProcessesForExecutable['git-tag'] = [ MockProcess( stdout: 'plugin1-v0.0.2\n' 'plugin2-v0.0.2\n') @@ -949,7 +955,7 @@ void main() { ])); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); @@ -995,7 +1001,7 @@ void main() { 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') @@ -1020,7 +1026,7 @@ void main() { 'However, the git release tag for this version (plugin2-v0.0.2) is not found.'), ])); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); @@ -1032,7 +1038,7 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('plugin1.dart').path}\n' '${pluginDir2.childFile('plugin2.dart').path}\n') @@ -1050,7 +1056,7 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); @@ -1081,7 +1087,7 @@ void main() { final Directory flutterPluginTools = createFakePlugin('flutter_plugin_tools', packagesDir); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: flutterPluginTools.childFile('pubspec.yaml').path) ]; @@ -1101,75 +1107,41 @@ void main() { ), isFalse); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); }); } -class TestProcessRunner extends ProcessRunner { +/// An extension of [RecordingProcessRunner] that stores 'flutter pub publish' +/// calls so that their input streams can be checked in tests. +class TestProcessRunner extends RecordingProcessRunner { // Most recent returned publish process. late MockProcess mockPublishProcess; - final List mockPublishArgs = []; - - String? mockPublishStdout; - String? mockPublishStderr; - int mockPublishCompleteCode = 0; - - @override - Future run( - String executable, - List args, { - Directory? workingDir, - bool exitOnError = false, - bool logOnError = false, - Encoding stdoutEncoding = io.systemEncoding, - Encoding stderrEncoding = io.systemEncoding, - }) async { - final io.ProcessResult result = io.Process.runSync(executable, args, - workingDirectory: workingDir?.path); - if (result.exitCode != 0) { - throw ToolExit(result.exitCode); - } - return result; - } @override Future start(String executable, List args, {Directory? workingDirectory}) async { - /// Never actually publish anything. Start is always and only used for this - /// since it returns something we can route stdin through. - assert(executable == getFlutterCommand(const LocalPlatform()) && + final io.Process process = + await super.start(executable, args, workingDirectory: workingDirectory); + if (executable == getFlutterCommand(const LocalPlatform()) && args.isNotEmpty && args[0] == 'pub' && - args[1] == 'publish'); - mockPublishArgs.addAll(args); - - mockPublishProcess = MockProcess( - exitCode: mockPublishCompleteCode, - stdout: mockPublishStdout, - stderr: mockPublishStderr, - stdoutEncoding: utf8, - stderrEncoding: utf8, - ); - return mockPublishProcess; + args[1] == 'publish') { + mockPublishProcess = process as MockProcess; + } + return process; } } class MockStdin extends Mock implements io.Stdin { List> mockUserInputs = >[]; - late StreamController> _controller; + final StreamController> _controller = StreamController>(); String? readLineOutput; @override Stream transform(StreamTransformer, S> streamTransformer) { - // In the test context, only one `PublishPluginCommand` object is created for a single test case. - // However, sometimes, we need to run multiple commands in a single test case. - // In such situation, this `MockStdin`'s StreamController might be listened to more than once, which is not allowed. - // - // Create a new controller every time so this Stdin could be listened to multiple times. - _controller = StreamController>(); mockUserInputs.forEach(_addUserInputsToSteam); return _controller.stream.transform(streamTransformer); } @@ -1189,12 +1161,3 @@ class MockStdin extends Mock implements io.Stdin { void _addUserInputsToSteam(List input) => _controller.add(input); } - -class MockProcessResult extends Mock implements io.ProcessResult { - MockProcessResult({int exitCode = 0}) : _exitCode = exitCode; - - final int _exitCode; - - @override - int get exitCode => _exitCode; -} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 05aebe82fd79..7bd94fb66e22 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -17,6 +17,8 @@ import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:quiver/collection.dart'; +import 'mocks.dart'; + /// Returns the exe name that command will use when running Flutter on /// [platform]. String getFlutterCommand(Platform platform) => @@ -320,7 +322,8 @@ class RecordingProcessRunner extends ProcessRunner { Future start(String executable, List args, {Directory? workingDirectory}) async { recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); - return Future.value(_getProcessToReturn(executable)); + return Future.value( + _getProcessToReturn(executable) ?? MockProcess()); } io.Process? _getProcessToReturn(String executable) { From a011b309b77c6b13c844c1dc6ed081327214573a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 26 Aug 2021 15:07:33 -0400 Subject: [PATCH 097/123] [flutter_plugin_tool] Add support for building UWP plugins (#4047) This allows building UWP plugin examples with `build-examples --winuwp`. As with previous pre-stable-template desktop support, this avoids the issue of unstable app templates by running `flutter create` on the fly before trying to build, so a template that will bitrot doesn't need to be checked in. Also adds no-op "support" for `drive-examples --winuwp`, with warnings about it not doing anything. This is to handle the fact that the LUCI recipe is shared between Win32 and UWP, and didn't conditionalize `drive`. Rather than change that, then change it back later, this just adds the no-op support now (since changing the tooling is much easier than changing LUCI recipes currently). This required some supporting tool changes: - Adds the ability to check for the new platform variants in a pubspec - Adds the ability to write test pubspecs that include variants, for testing Part of https://github.com/flutter/flutter/issues/82817 --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/build_examples_command.dart | 64 ++++++-- script/tool/lib/src/common/core.dart | 37 +++-- script/tool/lib/src/common/plugin_utils.dart | 39 ++++- .../tool/lib/src/drive_examples_command.dart | 26 ++- .../test/build_examples_command_test.dart | 129 ++++++++++++--- .../tool/test/common/plugin_utils_test.dart | 148 +++++++++++++----- .../create_all_plugins_app_command_test.dart | 2 +- .../test/drive_examples_command_test.dart | 112 ++++++++----- .../tool/test/lint_android_command_test.dart | 16 +- .../tool/test/native_test_command_test.dart | 124 +++++++-------- script/tool/test/test_command_test.dart | 4 +- script/tool/test/util.dart | 89 ++++++++--- .../tool/test/xcode_analyze_command_test.dart | 42 ++--- 14 files changed, 589 insertions(+), 244 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 1881d1bb6689..a32fb0016cb3 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -4,6 +4,7 @@ - Added a new `android-lint` command to lint Android plugin native code. - Pubspec validation now checks for `implements` in implementation packages. - Pubspec valitation now checks the full relative path of `repository` entries. +- `build-examples` now supports UWP plugins via a `--winuwp` flag. ## 0.5.0 diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index ac5e84b7c3c7..e441f61d5644 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -16,7 +16,16 @@ import 'common/repository_package.dart'; /// Key for APK. const String _platformFlagApk = 'apk'; -const int _exitNoPlatformFlags = 2; +const int _exitNoPlatformFlags = 3; + +// Flutter build types. These are the values passed to `flutter build `. +const String _flutterBuildTypeAndroid = 'apk'; +const String _flutterBuildTypeIos = 'ios'; +const String _flutterBuildTypeLinux = 'linux'; +const String _flutterBuildTypeMacOS = 'macos'; +const String _flutterBuildTypeWeb = 'web'; +const String _flutterBuildTypeWin32 = 'windows'; +const String _flutterBuildTypeWinUwp = 'winuwp'; /// A command to build the example applications for packages. class BuildExamplesCommand extends PackageLoopingCommand { @@ -30,6 +39,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { argParser.addFlag(kPlatformMacos); argParser.addFlag(kPlatformWeb); argParser.addFlag(kPlatformWindows); + argParser.addFlag(kPlatformWinUwp); argParser.addFlag(kPlatformIos); argParser.addFlag(_platformFlagApk); argParser.addOption( @@ -46,33 +56,40 @@ class BuildExamplesCommand extends PackageLoopingCommand { _platformFlagApk: const _PlatformDetails( 'Android', pluginPlatform: kPlatformAndroid, - flutterBuildType: 'apk', + flutterBuildType: _flutterBuildTypeAndroid, ), kPlatformIos: const _PlatformDetails( 'iOS', pluginPlatform: kPlatformIos, - flutterBuildType: 'ios', + flutterBuildType: _flutterBuildTypeIos, extraBuildFlags: ['--no-codesign'], ), kPlatformLinux: const _PlatformDetails( 'Linux', pluginPlatform: kPlatformLinux, - flutterBuildType: 'linux', + flutterBuildType: _flutterBuildTypeLinux, ), kPlatformMacos: const _PlatformDetails( 'macOS', pluginPlatform: kPlatformMacos, - flutterBuildType: 'macos', + flutterBuildType: _flutterBuildTypeMacOS, ), kPlatformWeb: const _PlatformDetails( 'web', pluginPlatform: kPlatformWeb, - flutterBuildType: 'web', + flutterBuildType: _flutterBuildTypeWeb, ), kPlatformWindows: const _PlatformDetails( - 'Windows', + 'Win32', + pluginPlatform: kPlatformWindows, + pluginPlatformVariant: platformVariantWin32, + flutterBuildType: _flutterBuildTypeWin32, + ), + kPlatformWinUwp: const _PlatformDetails( + 'UWP', pluginPlatform: kPlatformWindows, - flutterBuildType: 'windows', + pluginPlatformVariant: platformVariantWinUwp, + flutterBuildType: _flutterBuildTypeWinUwp, ), }; @@ -107,7 +124,8 @@ class BuildExamplesCommand extends PackageLoopingCommand { final Set<_PlatformDetails> buildPlatforms = <_PlatformDetails>{}; final Set<_PlatformDetails> unsupportedPlatforms = <_PlatformDetails>{}; for (final _PlatformDetails platform in requestedPlatforms) { - if (pluginSupportsPlatform(platform.pluginPlatform, package)) { + if (pluginSupportsPlatform(platform.pluginPlatform, package, + variant: platform.pluginPlatformVariant)) { buildPlatforms.add(platform); } else { unsupportedPlatforms.add(platform); @@ -156,6 +174,22 @@ class BuildExamplesCommand extends PackageLoopingCommand { }) async { final String enableExperiment = getStringArg(kEnableExperiment); + // The UWP template is not yet stable, so the UWP directory + // needs to be created on the fly with 'flutter create .' + Directory? temporaryPlatformDirectory; + if (flutterBuildType == _flutterBuildTypeWinUwp) { + final Directory uwpDirectory = example.directory.childDirectory('winuwp'); + if (!uwpDirectory.existsSync()) { + print('Creating temporary winuwp folder'); + final int exitCode = await processRunner.runAndStream(flutterCommand, + ['create', '--platforms=$kPlatformWinUwp', '.'], + workingDir: example.directory); + if (exitCode == 0) { + temporaryPlatformDirectory = uwpDirectory; + } + } + } + final int exitCode = await processRunner.runAndStream( flutterCommand, [ @@ -167,6 +201,13 @@ class BuildExamplesCommand extends PackageLoopingCommand { ], workingDir: example.directory, ); + + if (temporaryPlatformDirectory != null && + temporaryPlatformDirectory.existsSync()) { + print('Cleaning up ${temporaryPlatformDirectory.path}'); + temporaryPlatformDirectory.deleteSync(recursive: true); + } + return exitCode == 0; } } @@ -176,6 +217,7 @@ class _PlatformDetails { const _PlatformDetails( this.label, { required this.pluginPlatform, + this.pluginPlatformVariant, required this.flutterBuildType, this.extraBuildFlags = const [], }); @@ -186,6 +228,10 @@ class _PlatformDetails { /// The key in a pubspec's platform: entry. final String pluginPlatform; + /// The supportedVariants key under a plugin's [pluginPlatform] entry, if + /// applicable. + final String? pluginPlatformVariant; + /// The `flutter build` build type. final String flutterBuildType; diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index b2be8f56d172..53778eccb87f 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -10,24 +10,43 @@ import 'package:yaml/yaml.dart'; /// print destination. typedef Print = void Function(Object? object); -/// Key for windows platform. -const String kPlatformWindows = 'windows'; +/// Key for APK (Android) platform. +const String kPlatformAndroid = 'android'; -/// Key for macos platform. -const String kPlatformMacos = 'macos'; +/// Key for IPA (iOS) platform. +const String kPlatformIos = 'ios'; /// Key for linux platform. const String kPlatformLinux = 'linux'; -/// Key for IPA (iOS) platform. -const String kPlatformIos = 'ios'; - -/// Key for APK (Android) platform. -const String kPlatformAndroid = 'android'; +/// Key for macos platform. +const String kPlatformMacos = 'macos'; /// Key for Web platform. const String kPlatformWeb = 'web'; +/// Key for windows platform. +/// +/// Note that this corresponds to the Win32 variant for flutter commands like +/// `build` and `run`, but is a general platform containing all Windows +/// variants for purposes of the `platform` section of a plugin pubspec). +const String kPlatformWindows = 'windows'; + +/// Key for WinUWP platform. +/// +/// Note that UWP is a platform for the purposes of flutter commands like +/// `build` and `run`, but a variant of the `windows` platform for the purposes +/// of plugin pubspecs). +const String kPlatformWinUwp = 'winuwp'; + +/// Key for Win32 variant of the Windows platform. +const String platformVariantWin32 = 'win32'; + +/// Key for UWP variant of the Windows platform. +/// +/// See the note on [kPlatformWinUwp]. +const String platformVariantWinUwp = 'uwp'; + /// Key for enable experiment. const String kEnableExperiment = 'enable-experiment'; diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index d9c42e220c0b..49da67655e91 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -28,8 +28,12 @@ enum PlatformSupport { /// /// If [requiredMode] is provided, the plugin must have the given type of /// implementation in order to return true. -bool pluginSupportsPlatform(String platform, RepositoryPackage package, - {PlatformSupport? requiredMode}) { +bool pluginSupportsPlatform( + String platform, + RepositoryPackage package, { + PlatformSupport? requiredMode, + String? variant, +}) { assert(platform == kPlatformIos || platform == kPlatformAndroid || platform == kPlatformWeb || @@ -65,9 +69,34 @@ bool pluginSupportsPlatform(String platform, RepositoryPackage package, } // If the platform entry is present, then it supports the platform. Check // for required mode if specified. - final bool federated = platformEntry.containsKey('default_package'); - return requiredMode == null || - federated == (requiredMode == PlatformSupport.federated); + if (requiredMode != null) { + final bool federated = platformEntry.containsKey('default_package'); + if (federated != (requiredMode == PlatformSupport.federated)) { + return false; + } + } + + // If a variant is specified, check for that variant. + if (variant != null) { + const String variantsKey = 'supportedVariants'; + if (platformEntry.containsKey(variantsKey)) { + if (!(platformEntry['supportedVariants']! as YamlList) + .contains(variant)) { + return false; + } + } else { + // Platforms with variants have a default variant when unspecified for + // backward compatibility. Must match the flutter tool logic. + const Map defaultVariants = { + kPlatformWindows: platformVariantWin32, + }; + if (variant != defaultVariants[platform]) { + return false; + } + } + } + + return true; } on FileSystemException { return false; } on YamlException { diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 3605dcce1f22..b3434b0659f3 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -36,7 +36,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { argParser.addFlag(kPlatformWeb, help: 'Runs the web implementation of the examples'); argParser.addFlag(kPlatformWindows, - help: 'Runs the Windows implementation of the examples'); + help: 'Runs the Windows (Win32) implementation of the examples'); + argParser.addFlag(kPlatformWinUwp, + help: + 'Runs the UWP implementation of the examples [currently a no-op]'); argParser.addOption( kEnableExperiment, defaultsTo: '', @@ -67,6 +70,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { kPlatformMacos, kPlatformWeb, kPlatformWindows, + kPlatformWinUwp, ]; final int platformCount = platformSwitches .where((String platform) => getBoolArg(platform)) @@ -81,6 +85,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { throw ToolExit(_exitNoPlatformFlags); } + if (getBoolArg(kPlatformWinUwp)) { + logWarning('Driving UWP applications is not yet supported'); + } + String? androidDevice; if (getBoolArg(kPlatformAndroid)) { final List devices = await _getDevicesForPlatform('android'); @@ -116,6 +124,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { ], if (getBoolArg(kPlatformWindows)) kPlatformWindows: ['-d', 'windows'], + // TODO(stuartmorgan): Check these flags once drive supports UWP: + // https://github.com/flutter/flutter/issues/82821 + if (getBoolArg(kPlatformWinUwp)) + kPlatformWinUwp: ['-d', 'winuwp'], }; } @@ -132,7 +144,17 @@ class DriveExamplesCommand extends PackageLoopingCommand { final List deviceFlags = []; for (final MapEntry> entry in _targetDeviceFlags.entries) { - if (pluginSupportsPlatform(entry.key, package)) { + final String platform = entry.key; + String? variant; + if (platform == kPlatformWindows) { + variant = platformVariantWin32; + } else if (platform == kPlatformWinUwp) { + variant = platformVariantWinUwp; + // TODO(stuartmorgan): Remove this once drive supports UWP. + // https://github.com/flutter/flutter/issues/82821 + return PackageResult.skip('Drive does not yet support UWP'); + } + if (pluginSupportsPlatform(platform, package, variant: variant)) { deviceFlags.addAll(entry.value); } else { print('Skipping unsupported platform ${entry.key}...'); diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 9c7291c31ddb..a17107c18e27 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -56,8 +56,8 @@ void main() { test('fails if building fails', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }); processRunner @@ -106,8 +106,8 @@ void main() { test('building for iOS', () async { mockPlatform.isMacOS = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -163,8 +163,8 @@ void main() { test('building for Linux', () async { mockPlatform.isLinux = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformLinux: PlatformSupport.inline, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -212,8 +212,8 @@ void main() { test('building for macOS', () async { mockPlatform.isMacOS = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -258,8 +258,8 @@ void main() { test('building for web', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -284,7 +284,7 @@ void main() { }); test( - 'building for Windows when plugin is not set up for Windows results in no-op', + 'building for win32 when plugin is not set up for Windows results in no-op', () async { mockPlatform.isWindows = true; createFakePlugin('plugin', packagesDir); @@ -296,7 +296,7 @@ void main() { output, containsAllInOrder([ contains('Running for plugin'), - contains('Windows is not supported by this plugin'), + contains('Win32 is not supported by this plugin'), ]), ); @@ -305,11 +305,11 @@ void main() { expect(processRunner.recordedCalls, orderedEquals([])); }); - test('building for Windows', () async { + test('building for win32', () async { mockPlatform.isWindows = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformWindows: PlatformSupport.inline + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -321,7 +321,7 @@ void main() { expect( output, containsAllInOrder([ - '\nBUILDING plugin/example for Windows', + '\nBUILDING plugin/example for Win32 (windows)', ]), ); @@ -335,6 +335,91 @@ void main() { ])); }); + test('building for UWP when plugin does not support UWP is a no-op', + () async { + createFakePlugin('plugin', packagesDir); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--winuwp']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('UWP is not supported by this plugin'), + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for UWP', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.federated, + variants: [platformVariantWinUwp]), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--winuwp']); + + expect( + output, + containsAllInOrder([ + contains('BUILDING plugin/example for UWP (winuwp)'), + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'winuwp'], pluginExampleDirectory.path), + ])); + }); + + test('building for UWP creates a folder if necessary', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.federated, + variants: [platformVariantWinUwp]), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--winuwp']); + + expect( + output, + contains('Creating temporary winuwp folder'), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['create', '--platforms=winuwp', '.'], + pluginExampleDirectory.path), + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'winuwp'], pluginExampleDirectory.path), + ])); + }); + test( 'building for Android when plugin is not set up for Android results in no-op', () async { @@ -358,8 +443,8 @@ void main() { test('building for Android', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -387,8 +472,8 @@ void main() { test('enable-experiment flag for Android', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -409,8 +494,8 @@ void main() { test('enable-experiment flag for ios', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart index 7f1ba2add00a..2e08f725eb4b 100644 --- a/script/tool/test/common/plugin_utils_test.dart +++ b/script/tool/test/common/plugin_utils_test.dart @@ -36,13 +36,13 @@ void main() { test('all platforms', () async { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - kPlatformLinux: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, - kPlatformWeb: PlatformSupport.inline, - kPlatformWindows: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), })); expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); @@ -55,14 +55,12 @@ void main() { test('some platforms', () async { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformLinux: PlatformSupport.inline, - kPlatformWeb: PlatformSupport.inline, - }, - )); + 'plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + })); expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); @@ -74,17 +72,15 @@ void main() { test('inline plugins are only detected as inline', () async { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - kPlatformLinux: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, - kPlatformWeb: PlatformSupport.inline, - kPlatformWindows: PlatformSupport.inline, - }, - )); + 'plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + })); expect( pluginSupportsPlatform(kPlatformAndroid, plugin, @@ -137,19 +133,16 @@ void main() { }); test('federated plugins are only detected as federated', () async { - const String pluginName = 'plugin'; final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - pluginName, - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.federated, - kPlatformIos: PlatformSupport.federated, - kPlatformLinux: PlatformSupport.federated, - kPlatformMacos: PlatformSupport.federated, - kPlatformWeb: PlatformSupport.federated, - kPlatformWindows: PlatformSupport.federated, - }, - )); + 'plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.federated), + kPlatformIos: const PlatformDetails(PlatformSupport.federated), + kPlatformLinux: const PlatformDetails(PlatformSupport.federated), + kPlatformMacos: const PlatformDetails(PlatformSupport.federated), + kPlatformWeb: const PlatformDetails(PlatformSupport.federated), + kPlatformWindows: const PlatformDetails(PlatformSupport.federated), + })); expect( pluginSupportsPlatform(kPlatformAndroid, plugin, @@ -200,5 +193,84 @@ void main() { requiredMode: PlatformSupport.inline), isFalse); }); + + test('windows without variants is only win32', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }, + )); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isFalse); + }); + + test('windows with both variants matches win32 and winuwp', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails( + PlatformSupport.federated, + variants: [platformVariantWin32, platformVariantWinUwp], + ), + })); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isTrue); + }); + + test('win32 plugin is only win32', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails( + PlatformSupport.federated, + variants: [platformVariantWin32], + ), + })); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isFalse); + }); + + test('winup plugin is only winuwp', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.federated, + variants: [platformVariantWinUwp]), + }, + )); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isFalse); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isTrue); + }); }); } diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index 4439d13c3625..0066cc53f61a 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -21,7 +21,7 @@ void main() { setUp(() { // Since the core of this command is a call to 'flutter create', the test // has to use the real filesystem. Put everything possible in a unique - // temporary to minimize affect on the host system. + // temporary to minimize effect on the host system. fileSystem = const LocalFileSystem(); testRoot = fileSystem.systemTempDirectory.createTempSync(); packagesDir = testRoot.childDirectory('packages'); diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index bbf865d3edf2..85d2326d0689 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -127,8 +127,8 @@ void main() { 'example/test_driver/integration_test.dart', 'example/integration_test/foo_test.dart', ], - platformSupport: { - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -192,9 +192,9 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -242,9 +242,9 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -275,9 +275,9 @@ void main() { extraFiles: [ 'example/lib/main.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -311,9 +311,9 @@ void main() { 'example/integration_test/foo_test.dart', 'example/integration_test/ignore_me.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -397,8 +397,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformLinux: PlatformSupport.inline, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), }, ); @@ -470,8 +470,8 @@ void main() { 'example/test_driver/plugin.dart', 'example/macos/macos.swift', ], - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -541,8 +541,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -615,8 +615,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformWindows: PlatformSupport.inline + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), }, ); @@ -654,6 +654,40 @@ void main() { ])); }); + test('driving UWP is a no-op', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + variants: [platformVariantWinUwp]), + }, + ); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--winuwp', + ]); + + expect( + output, + containsAllInOrder([ + contains('Driving UWP applications is not yet supported'), + contains('Running for plugin'), + contains('SKIPPING: Drive does not yet support UWP'), + contains('No issues found!'), + ]), + ); + + // Output should be empty since running drive-examples --windows on a + // non-Windows plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + test('driving on an Android plugin', () async { final Directory pluginDirectory = createFakePlugin( 'plugin', @@ -662,8 +696,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), }, ); @@ -712,8 +746,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -745,8 +779,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -800,9 +834,9 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -842,8 +876,8 @@ void main() { 'plugin', packagesDir, examples: [], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -874,8 +908,8 @@ void main() { 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', ], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -906,8 +940,8 @@ void main() { extraFiles: [ 'example/test_driver/integration_test.dart', ], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -942,8 +976,8 @@ void main() { 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', ], - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }, ); diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart index d08058468636..5670a64f30d8 100644 --- a/script/tool/test/lint_android_command_test.dart +++ b/script/tool/test/lint_android_command_test.dart @@ -43,8 +43,8 @@ void main() { final Directory pluginDir = createFakePlugin('plugin1', packagesDir, extraFiles: [ 'example/android/gradlew', - ], platformSupport: { - kPlatformAndroid: PlatformSupport.inline + ], platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }); final Directory androidDir = @@ -74,8 +74,8 @@ void main() { test('fails if gradlew is missing', () async { createFakePlugin('plugin1', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }); Error? commandError; @@ -96,8 +96,8 @@ void main() { test('fails if linting finds issues', () async { createFakePlugin('plugin1', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }); processRunner.mockProcessesForExecutable['gradlew'] = [ @@ -138,8 +138,8 @@ void main() { test('skips non-inline plugins', () async { createFakePlugin('plugin1', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.federated + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.federated) }); final List output = diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index f367dc80182f..7b2a3d3ba39c 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -115,8 +115,8 @@ void main() { test('reports skips with no tests', () async { final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -154,8 +154,8 @@ void main() { group('iOS', () { test('skip if iOS is not supported', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final List output = await runCapturingPrint(runner, @@ -171,8 +171,8 @@ void main() { test('skip if iOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.federated + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.federated) }); final List output = await runCapturingPrint(runner, @@ -188,8 +188,8 @@ void main() { test('running with correct destination', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = @@ -234,8 +234,8 @@ void main() { test('Not specifying --ios-destination assigns an available simulator', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -298,8 +298,8 @@ void main() { test('skip if macOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.federated, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.federated), }); final List output = @@ -317,8 +317,8 @@ void main() { test('runs for macOS plugin', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -360,8 +360,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -390,8 +390,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -420,8 +420,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -455,8 +455,8 @@ void main() { createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -480,8 +480,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -519,8 +519,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -554,8 +554,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -586,8 +586,8 @@ void main() { createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/app/src/test/example_test.java', @@ -618,8 +618,8 @@ void main() { createFakePlugin( 'plugin1', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -630,8 +630,8 @@ void main() { createFakePlugin( 'plugin2', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -657,8 +657,8 @@ void main() { final Directory pluginDir = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -716,8 +716,8 @@ void main() { createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, ); @@ -739,8 +739,8 @@ void main() { group('iOS/macOS', () { test('fails if xcrun fails', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -767,8 +767,8 @@ void main() { test('honors unit-only', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -832,8 +832,8 @@ void main() { test('honors integration-only', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -897,8 +897,8 @@ void main() { test('skips when the requested target is not present', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -950,8 +950,8 @@ void main() { test('fails if unable to check for requested target', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -1007,10 +1007,10 @@ void main() { 'example/android/gradlew', 'android/src/test/example_test.java', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -1077,8 +1077,8 @@ void main() { test('runs only macOS for a macOS plugin', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -1121,8 +1121,8 @@ void main() { test('runs only iOS for a iOS plugin', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = @@ -1193,9 +1193,9 @@ void main() { final Directory pluginDir = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, extraFiles: [ 'example/android/gradlew', @@ -1244,9 +1244,9 @@ void main() { final Directory pluginDir = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, extraFiles: [ 'example/android/gradlew', diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 3b350f7d88ae..f8aca38d3478 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -180,8 +180,8 @@ void main() { 'plugin', packagesDir, extraFiles: ['test/empty_test.dart'], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }, ); diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 7bd94fb66e22..9b92a5d94ac8 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -41,6 +41,21 @@ Directory createPackagesDirectory( return packagesDir; } +/// Details for platform support in a plugin. +@immutable +class PlatformDetails { + const PlatformDetails( + this.type, { + this.variants = const [], + }); + + /// The type of support for the platform. + final PlatformSupport type; + + /// Any 'supportVariants' to list in the pubspec. + final List variants; +} + /// Creates a plugin package with the given [name] in [packagesDirectory]. /// /// [platformSupport] is a map of platform string to the support details for @@ -54,8 +69,8 @@ Directory createFakePlugin( Directory parentDirectory, { List examples = const ['example'], List extraFiles = const [], - Map platformSupport = - const {}, + Map platformSupport = + const {}, String? version = '0.0.1', }) { final Directory pluginDirectory = createFakePackage(name, parentDirectory, @@ -143,8 +158,8 @@ void createFakePubspec( String name = 'fake_package', bool isFlutter = true, bool isPlugin = false, - Map platformSupport = - const {}, + Map platformSupport = + const {}, String publishTo = 'http://no_pub_server.com', String? version, }) { @@ -160,12 +175,11 @@ flutter: plugin: platforms: '''; - for (final MapEntry platform + for (final MapEntry platform in platformSupport.entries) { yaml += _pluginPlatformSection(platform.key, platform.value, name); } } - yaml += ''' dependencies: flutter: @@ -186,50 +200,73 @@ publish_to: $publishTo # Hardcoded safeguard to prevent this from somehow being } String _pluginPlatformSection( - String platform, PlatformSupport type, String packageName) { - if (type == PlatformSupport.federated) { - return ''' + String platform, PlatformDetails support, String packageName) { + String entry = ''; + // Build the main plugin entry. + if (support.type == PlatformSupport.federated) { + entry = ''' $platform: default_package: ${packageName}_$platform '''; - } - switch (platform) { - case kPlatformAndroid: - return ''' + } else { + switch (platform) { + case kPlatformAndroid: + entry = ''' android: package: io.flutter.plugins.fake pluginClass: FakePlugin '''; - case kPlatformIos: - return ''' + break; + case kPlatformIos: + entry = ''' ios: pluginClass: FLTFakePlugin '''; - case kPlatformLinux: - return ''' + break; + case kPlatformLinux: + entry = ''' linux: pluginClass: FakePlugin '''; - case kPlatformMacos: - return ''' + break; + case kPlatformMacos: + entry = ''' macos: pluginClass: FakePlugin '''; - case kPlatformWeb: - return ''' + break; + case kPlatformWeb: + entry = ''' web: pluginClass: FakePlugin fileName: ${packageName}_web.dart '''; - case kPlatformWindows: - return ''' + break; + case kPlatformWindows: + entry = ''' windows: pluginClass: FakePlugin '''; - default: - assert(false); - return ''; + break; + default: + assert(false, 'Unrecognized platform: $platform'); + break; + } } + + // Add any variants. + if (support.variants.isNotEmpty) { + entry += ''' + supportedVariants: +'''; + for (final String variant in support.variants) { + entry += ''' + - $variant +'''; + } + } + + return entry; } typedef _ErrorHandler = void Function(Error error); diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart index 790a526a8ae0..10008ae33a11 100644 --- a/script/tool/test/xcode_analyze_command_test.dart +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -57,8 +57,8 @@ void main() { group('iOS', () { test('skip if iOS is not supported', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final List output = @@ -70,8 +70,8 @@ void main() { test('skip if iOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.federated + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.federated) }); final List output = @@ -83,8 +83,8 @@ void main() { test('runs for iOS plugin', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = @@ -126,8 +126,8 @@ void main() { test('fails if xcrun fails', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -172,8 +172,8 @@ void main() { test('skip if macOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.federated, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.federated), }); final List output = await runCapturingPrint( @@ -186,8 +186,8 @@ void main() { test('runs for macOS plugin', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -223,8 +223,8 @@ void main() { test('fails if xcrun fails', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -253,9 +253,9 @@ void main() { test('runs both iOS and macOS when supported', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -313,8 +313,8 @@ void main() { test('runs only macOS for a macOS plugin', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -354,8 +354,8 @@ void main() { test('runs only iOS for a iOS plugin', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = From 8e8954731570a8088c7398884908c67b38b35a8d Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 26 Aug 2021 15:56:59 -0400 Subject: [PATCH 098/123] Disable some flaky tests (#4274) These tests are failing frequently, and interfering with the tree staying open. Tracking issues: https://github.com/flutter/flutter/issues/88837 https://github.com/flutter/flutter/issues/86915 https://github.com/flutter/flutter/issues/86757 --- .../plugins/androidalarmmanager/BackgroundExecutionTest.java | 2 ++ .../example/integration_test/video_player_test.dart | 5 ++++- .../example/integration_test/webview_flutter_test.dart | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/BackgroundExecutionTest.java b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/BackgroundExecutionTest.java index d6927232fb80..a841a239d3af 100644 --- a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/BackgroundExecutionTest.java +++ b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/BackgroundExecutionTest.java @@ -17,6 +17,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.rule.ActivityTestRule; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,6 +40,7 @@ public void setUp() throws Exception { ActivityScenario.launch(DriverExtensionActivity.class); } + @Ignore("Disabled due to flake: https://github.com/flutter/flutter/issues/88837") @Test public void startBackgroundIsolate() throws Exception { diff --git a/packages/video_player/video_player/example/integration_test/video_player_test.dart b/packages/video_player/video_player/example/integration_test/video_player_test.dart index 6821b26e0409..373538ad365e 100644 --- a/packages/video_player/video_player/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player/example/integration_test/video_player_test.dart @@ -220,6 +220,9 @@ void main() { await tester.pumpAndSettle(); expect(_controller.value.isPlaying, true); - }, skip: kIsWeb); // Web does not support local assets. + }, + skip: kIsWeb || // Web does not support local assets. + // Extremely flaky on iOS: https://github.com/flutter/flutter/issues/86915 + defaultTargetPlatform == TargetPlatform.iOS); }); } diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart index f3eeee156421..0e128caa8f32 100644 --- a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -1330,7 +1330,9 @@ void main() { await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, 'https://flutter.dev/'); - }); + }, + // Flaky on Android: https://github.com/flutter/flutter/issues/86757 + skip: Platform.isAndroid); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets( From 46dd6093793aa19af45244cd7fc16cc5b0ee2082 Mon Sep 17 00:00:00 2001 From: Majid Hajian Date: Thu, 26 Aug 2021 23:21:07 +0200 Subject: [PATCH 099/123] [camera] Replace device info with new device_info_plus (#4265) --- packages/camera/camera/CHANGELOG.md | 4 ++++ packages/camera/camera/README.md | 2 +- packages/camera/camera/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 73cce2c539c1..6d38fa204540 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.1+1 + +* Replace `device_info` reference with `device_info_plus` in the [README.md](README.md) + ## 0.9.1 * Added `lensAperture`, `sensorExposureTime` and `sensorSensitivity` properties to the `CameraImage` dto. diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md index fb6144face9b..c66ed67af6cb 100644 --- a/packages/camera/camera/README.md +++ b/packages/camera/camera/README.md @@ -19,7 +19,7 @@ First, add `camera` as a [dependency in your pubspec.yaml file](https://flutter. ### iOS -iOS 10.0 of higher is needed to use the camera plugin. If compiling for any version lower than 10.0 make sure to check the iOS version before using the camera plugin. For example, using the [device_info](https://pub.dev/packages/device_info) plugin. +iOS 10.0 of higher is needed to use the camera plugin. If compiling for any version lower than 10.0 make sure to check the iOS version before using the camera plugin. For example, using the [device_info_plus](https://pub.dev/packages/device_info_plus) plugin. Add two rows to the `ios/Runner/Info.plist`: diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 08d1e3eead4f..1009191e771e 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.1 +version: 0.9.1+1 environment: sdk: ">=2.12.0 <3.0.0" From 1f502e8b6f0082a0f2e2e89e223b7f2de899177e Mon Sep 17 00:00:00 2001 From: BeMacized Date: Fri, 27 Aug 2021 10:26:04 +0200 Subject: [PATCH 100/123] [camera] Add Android & iOS implementations for pausing the camera preview (#4258) --- packages/camera/camera/CHANGELOG.md | 4 + .../io/flutter/plugins/camera/Camera.java | 78 ++++++---- .../plugins/camera/MethodCallHandlerImpl.java | 16 ++ .../io/flutter/plugins/camera/CameraTest.java | 27 ++++ .../camera/MethodCallHandlerImplTest.java | 69 +++++++++ .../plugins/camera/utils/TestUtils.java | 11 ++ .../ios/Runner.xcodeproj/project.pbxproj | 4 + .../ios/RunnerTests/CameraPreviewPauseTests.m | 50 +++++++ packages/camera/camera/example/lib/main.dart | 28 +++- .../camera/camera/ios/Classes/CameraPlugin.m | 19 ++- .../camera/lib/src/camera_controller.dart | 48 +++++- .../camera/camera/lib/src/camera_preview.dart | 3 +- packages/camera/camera/pubspec.yaml | 4 +- .../camera/test/camera_preview_test.dart | 6 + packages/camera/camera/test/camera_test.dart | 140 ++++++++++++++++++ .../camera/camera/test/camera_value_test.dart | 43 +++--- 16 files changed, 498 insertions(+), 52 deletions(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java create mode 100644 packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 6d38fa204540..bb0048036f58 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.2 + +* Added functions to pause and resume the camera preview. + ## 0.9.1+1 * Replace `device_info` reference with `device_info_plus` in the [README.md](README.md) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 43479aca616c..c036c1c7e9d3 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -126,6 +126,8 @@ class Camera private MediaRecorder mediaRecorder; /** True when recording video. */ private boolean recordingVideo; + /** True when the preview is paused. */ + private boolean pausedPreview; private File captureFile; @@ -428,8 +430,10 @@ private void refreshPreviewCaptureSession( } try { - captureSession.setRepeatingRequest( - previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + if (!pausedPreview) { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + } if (onSuccessCallback != null) { onSuccessCallback.run(); @@ -834,33 +838,36 @@ public void setFocusMode(final Result result, @NonNull FocusMode newMode) { * For focus mode an extra step of actually locking/unlocking the * focus has to be done, in order to ensure it goes into the correct state. */ - switch (newMode) { - case locked: - // Perform a single focus trigger. - lockAutoFocus(); - if (captureSession == null) { - Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); - return; - } - - // Set AF state to idle again. - previewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); - - try { - captureSession.setRepeatingRequest( - previewRequestBuilder.build(), null, backgroundHandler); - } catch (CameraAccessException e) { - if (result != null) { - result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + if (!pausedPreview) { + switch (newMode) { + case locked: + // Perform a single focus trigger. + if (captureSession == null) { + Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); + return; + } + lockAutoFocus(); + + // Set AF state to idle again. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + try { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + if (result != null) { + result.error( + "setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + } + return; } - return; - } - break; - case auto: - // Cancel current AF trigger and set AF to idle again. - unlockAutoFocus(); - break; + break; + case auto: + // Cancel current AF trigger and set AF to idle again. + unlockAutoFocus(); + break; + } } if (result != null) { @@ -966,6 +973,19 @@ public void unlockCaptureOrientation() { cameraFeatures.getSensorOrientation().unlockCaptureOrientation(); } + /** Pause the preview from dart. */ + public void pausePreview() throws CameraAccessException { + this.pausedPreview = true; + this.captureSession.stopRepeating(); + } + + /** Resume the preview from dart. */ + public void resumePreview() { + this.pausedPreview = false; + this.refreshPreviewCaptureSession( + null, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } + public void startPreview() throws CameraAccessException { if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; Log.i(TAG, "startPreview"); @@ -1022,8 +1042,8 @@ public void onError(String errorCode, String errorMessage) { private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { imageStreamReader.setOnImageAvailableListener( reader -> { - // Use acquireNextImage since image reader is only for one image. Image img = reader.acquireNextImage(); + // Use acquireNextImage since image reader is only for one image. if (img == null) return; List> planes = new ArrayList<>(); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 893785f1a58f..5e25353cbca9 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -339,6 +339,22 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) } break; } + case "pausePreview": + { + try { + camera.pausePreview(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "resumePreview": + { + camera.resumePreview(); + result.success(null); + break; + } case "dispose": { if (camera != null) { diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java index cab2ae8974a4..5431df0df636 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -744,6 +744,33 @@ public void unlockCaptureOrientation_shouldUnlockCaptureOrientation() { verify(mockSensorOrientationFeature, times(1)).unlockCaptureOrientation(); } + @Test + public void pausePreview_shouldPausePreview() throws CameraAccessException { + camera.pausePreview(); + + assertEquals(TestUtils.getPrivateField(camera, "pausedPreview"), true); + verify(mockCaptureSession, times(1)).stopRepeating(); + } + + @Test + public void resumePreview_shouldResumePreview() throws CameraAccessException { + camera.resumePreview(); + + assertEquals(TestUtils.getPrivateField(camera, "pausedPreview"), false); + verify(mockCaptureSession, times(1)).setRepeatingRequest(any(), any(), any()); + } + + @Test + public void resumePreview_shouldSendErrorEventOnCameraAccessException() + throws CameraAccessException { + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0)); + + camera.resumePreview(); + + verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any()); + } + private static class TestCameraFeatureFactory implements CameraFeatureFactory { private final AutoFocusFeature mockAutoFocusFeature; private final ExposureLockFeature mockExposureLockFeature; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java new file mode 100644 index 000000000000..35eed7a66a1a --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java @@ -0,0 +1,69 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.Activity; +import android.hardware.camera2.CameraAccessException; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.camera.utils.TestUtils; +import io.flutter.view.TextureRegistry; +import org.junit.Before; +import org.junit.Test; + +public class MethodCallHandlerImplTest { + + MethodChannel.MethodCallHandler handler; + MethodChannel.Result mockResult; + Camera mockCamera; + + @Before + public void setUp() { + handler = + new MethodCallHandlerImpl( + mock(Activity.class), + mock(BinaryMessenger.class), + mock(CameraPermissions.class), + mock(CameraPermissions.PermissionsRegistry.class), + mock(TextureRegistry.class), + null); + mockResult = mock(MethodChannel.Result.class); + mockCamera = mock(Camera.class); + TestUtils.setPrivateField(handler, "camera", mockCamera); + } + + @Test + public void onMethodCall_pausePreview_shouldPausePreviewAndSendSuccessResult() + throws CameraAccessException { + handler.onMethodCall(new MethodCall("pausePreview", null), mockResult); + + verify(mockCamera, times(1)).pausePreview(); + verify(mockResult, times(1)).success(null); + } + + @Test + public void onMethodCall_pausePreview_shouldSendErrorResultOnCameraAccessException() + throws CameraAccessException { + doThrow(new CameraAccessException(0)).when(mockCamera).pausePreview(); + + handler.onMethodCall(new MethodCall("pausePreview", null), mockResult); + + verify(mockResult, times(1)).error("CameraAccess", null, null); + } + + @Test + public void onMethodCall_resumePreview_shouldResumePreviewAndSendSuccessResult() { + handler.onMethodCall(new MethodCall("resumePreview", null), mockResult); + + verify(mockCamera, times(1)).resumePreview(); + verify(mockResult, times(1)).success(null); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java index dbf9d11be8b6..fce99b54384b 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java @@ -33,4 +33,15 @@ public static void setPrivateField(T instance, String fieldName, Object newV Assert.fail("Unable to mock private field: " + fieldName); } } + + public static Object getPrivateField(T instance, String fieldName) { + try { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(instance); + } catch (Exception e) { + Assert.fail("Unable to mock private field: " + fieldName); + return null; + } + } } diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index aead167a5e99..5a622f17fc63 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; A513685080F868CF2695CE75 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */; }; D065CD815D405ECB22FB1BBA /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */; }; + E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -68,6 +69,7 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A4725B4F24805CD3CA67828F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; D1FF8C34CA9E9BE702C5EC06 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -96,6 +98,7 @@ 03BB766A2665316900CE5A93 /* CameraFocusTests.m */, 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */, 03BB766C2665316900CE5A93 /* Info.plist */, + E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, ); path = RunnerTests; sourceTree = ""; @@ -359,6 +362,7 @@ buildActionMask = 2147483647; files = ( 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, + E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m new file mode 100644 index 000000000000..549b40a52e46 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m @@ -0,0 +1,50 @@ +// 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 camera; +@import XCTest; +@import AVFoundation; +#import + +@interface FLTCam : NSObject +@property(assign, nonatomic) BOOL isPreviewPaused; +- (void)pausePreviewWithResult:(FlutterResult)result; +- (void)resumePreviewWithResult:(FlutterResult)result; +@end + +@interface CameraPreviewPauseTests : XCTestCase +@property(readonly, nonatomic) FLTCam* camera; +@end + +@implementation CameraPreviewPauseTests + +- (void)setUp { + _camera = [[FLTCam alloc] init]; +} + +- (void)testPausePreviewWithResult_shouldPausePreview { + XCTestExpectation* resultExpectation = + [self expectationWithDescription:@"Succeeding result with nil value"]; + [_camera pausePreviewWithResult:^void(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:2.0 handler:nil]; + XCTAssertTrue(_camera.isPreviewPaused); +} + +- (void)testResumePreviewWithResult_shouldResumePreview { + XCTestExpectation* resultExpectation = + [self expectationWithDescription:@"Succeeding result with nil value"]; + [_camera resumePreviewWithResult:^void(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:2.0 handler:nil]; + XCTAssertFalse(_camera.isPreviewPaused); +} + +@end diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 2314aecbece3..364f59d81356 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -530,7 +530,16 @@ class _CameraExampleHomeState extends State cameraController.value.isRecordingVideo ? onStopButtonPressed : null, - ) + ), + IconButton( + icon: const Icon(Icons.pause_presentation), + color: + cameraController != null && cameraController.value.isPreviewPaused + ? Colors.red + : Colors.blue, + onPressed: + cameraController == null ? null : onPausePreviewButtonPressed, + ), ], ); } @@ -747,6 +756,23 @@ class _CameraExampleHomeState extends State }); } + Future onPausePreviewButtonPressed() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return; + } + + if (cameraController.value.isPreviewPaused) { + await cameraController.resumePreview(); + } else { + await cameraController.pausePreview(); + } + + if (mounted) setState(() {}); + } + void onPauseButtonPressed() { pauseVideoRecording().then((_) { if (mounted) setState(() {}); diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index ea03ce57649c..cb93e9f5349d 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -330,6 +330,7 @@ @interface FLTCam : NSObject isRecordingVideo && _isRecordingPaused; @@ -150,6 +159,8 @@ class CameraValue { DeviceOrientation? deviceOrientation, Optional? lockedCaptureOrientation, Optional? recordingOrientation, + bool? isPreviewPaused, + Optional? previewPauseOrientation, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -172,6 +183,10 @@ class CameraValue { recordingOrientation: recordingOrientation == null ? this.recordingOrientation : recordingOrientation.orNull, + isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, + previewPauseOrientation: previewPauseOrientation == null + ? this.previewPauseOrientation + : previewPauseOrientation.orNull, ); } @@ -190,7 +205,9 @@ class CameraValue { 'focusPointSupported: $focusPointSupported, ' 'deviceOrientation: $deviceOrientation, ' 'lockedCaptureOrientation: $lockedCaptureOrientation, ' - 'recordingOrientation: $recordingOrientation)'; + 'recordingOrientation: $recordingOrientation, ' + 'isPreviewPaused: $isPreviewPaused, ' + 'previewPausedOrientation: $previewPauseOrientation)'; } } @@ -325,6 +342,35 @@ class CameraController extends ValueNotifier { await CameraPlatform.instance.prepareForVideoRecording(); } + /// Pauses the current camera preview + Future pausePreview() async { + if (value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.pausePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: true, + previewPauseOrientation: Optional.of(this.value.deviceOrientation)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Resumes the current camera preview + Future resumePreview() async { + if (!value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.resumePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: false, previewPauseOrientation: Optional.absent()); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + /// Captures an image and returns the file where it was saved. /// /// Throws a [CameraException] if the capture fails. diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index 1df9f8e2e393..6a15896bfa47 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -71,7 +71,8 @@ class CameraPreview extends StatelessWidget { DeviceOrientation _getApplicableOrientation() { return controller.value.isRecordingVideo ? controller.value.recordingOrientation! - : (controller.value.lockedCaptureOrientation ?? + : (controller.value.previewPauseOrientation ?? + controller.value.lockedCaptureOrientation ?? controller.value.deviceOrientation); } } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 1009191e771e..3e3fad15051b 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.1+1 +version: 0.9.2 environment: sdk: ">=2.12.0 <3.0.0" @@ -20,7 +20,7 @@ flutter: pluginClass: CameraPlugin dependencies: - camera_platform_interface: ^2.0.0 + camera_platform_interface: ^2.1.0 flutter: sdk: flutter pedantic: ^1.10.0 diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 8275461192b4..14afddaea070 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -113,6 +113,12 @@ class FakeController extends ValueNotifier @override Future unlockCaptureOrientation() async {} + + @override + Future pausePreview() async {} + + @override + Future resumePreview() async {} } void main() { diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 26382a9b7d60..6904e68ef89f 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -1137,6 +1137,138 @@ void main() { .called(4); }); + test('pausePreview() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = cameraController.value + .copyWith(deviceOrientation: DeviceOrientation.portraitUp); + + await cameraController.pausePreview(); + + verify(CameraPlatform.instance.pausePreview(cameraController.cameraId)) + .called(1); + expect(cameraController.value.isPreviewPaused, equals(true)); + expect(cameraController.value.previewPauseOrientation, + DeviceOrientation.portraitUp); + }); + + test('pausePreview() does not call $CameraPlatform when already paused', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = + cameraController.value.copyWith(isPreviewPaused: true); + + await cameraController.pausePreview(); + + verifyNever( + CameraPlatform.instance.pausePreview(cameraController.cameraId)); + expect(cameraController.value.isPreviewPaused, equals(true)); + }); + + test('pausePreview() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance.pausePreview(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.pausePreview(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('resumePreview() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = + cameraController.value.copyWith(isPreviewPaused: true); + + await cameraController.resumePreview(); + + verify(CameraPlatform.instance.resumePreview(cameraController.cameraId)) + .called(1); + expect(cameraController.value.isPreviewPaused, equals(false)); + }); + + test('resumePreview() does not call $CameraPlatform when not paused', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = + cameraController.value.copyWith(isPreviewPaused: false); + + await cameraController.resumePreview(); + + verifyNever( + CameraPlatform.instance.resumePreview(cameraController.cameraId)); + expect(cameraController.value.isPreviewPaused, equals(false)); + }); + + test('resumePreview() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = + cameraController.value.copyWith(isPreviewPaused: true); + when(CameraPlatform.instance.resumePreview(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.resumePreview(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + test('lockCaptureOrientation() calls $CameraPlatform', () async { CameraController cameraController = CameraController( CameraDescription( @@ -1314,6 +1446,14 @@ class MockCameraPlatform extends Mock Future unlockCaptureOrientation(int? cameraId) async => super .noSuchMethod(Invocation.method(#unlockCaptureOrientation, [cameraId])); + @override + Future pausePreview(int? cameraId) async => + super.noSuchMethod(Invocation.method(#pausePreview, [cameraId])); + + @override + Future resumePreview(int? cameraId) async => + super.noSuchMethod(Invocation.method(#resumePreview, [cameraId])); + @override Future getMaxZoomLevel(int? cameraId) async => super.noSuchMethod( Invocation.method(#getMaxZoomLevel, [cameraId]), diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index e0378cca2cb9..4718d8943c34 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -29,6 +29,8 @@ void main() { lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, focusPointSupported: true, + isPreviewPaused: false, + previewPauseOrientation: DeviceOrientation.portraitUp, ); expect(cameraValue, isA()); @@ -46,6 +48,8 @@ void main() { expect( cameraValue.lockedCaptureOrientation, DeviceOrientation.portraitUp); expect(cameraValue.recordingOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.isPreviewPaused, false); + expect(cameraValue.previewPauseOrientation, DeviceOrientation.portraitUp); }); test('Can be created as uninitialized', () { @@ -66,6 +70,8 @@ void main() { expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); expect(cameraValue.lockedCaptureOrientation, null); expect(cameraValue.recordingOrientation, null); + expect(cameraValue.isPreviewPaused, isFalse); + expect(cameraValue.previewPauseOrientation, null); }); test('Can be copied with isInitialized', () { @@ -87,6 +93,8 @@ void main() { expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); expect(cameraValue.lockedCaptureOrientation, null); expect(cameraValue.recordingOrientation, null); + expect(cameraValue.isPreviewPaused, isFalse); + expect(cameraValue.previewPauseOrientation, null); }); test('Has aspectRatio after setting size', () { @@ -117,25 +125,26 @@ void main() { test('toString() works as expected', () { var cameraValue = const CameraValue( - isInitialized: false, - errorDescription: null, - previewSize: Size(10, 10), - isRecordingPaused: false, - isRecordingVideo: false, - isTakingPicture: false, - isStreamingImages: false, - flashMode: FlashMode.auto, - exposureMode: ExposureMode.auto, - focusMode: FocusMode.auto, - exposurePointSupported: true, - focusPointSupported: true, - deviceOrientation: DeviceOrientation.portraitUp, - lockedCaptureOrientation: DeviceOrientation.portraitUp, - recordingOrientation: DeviceOrientation.portraitUp, - ); + isInitialized: false, + errorDescription: null, + previewSize: Size(10, 10), + isRecordingPaused: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + focusMode: FocusMode.auto, + exposurePointSupported: true, + focusPointSupported: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: DeviceOrientation.portraitUp, + recordingOrientation: DeviceOrientation.portraitUp, + isPreviewPaused: true, + previewPauseOrientation: DeviceOrientation.portraitUp); expect(cameraValue.toString(), - 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp)'); + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, isPreviewPaused: true, previewPausedOrientation: DeviceOrientation.portraitUp)'); }); }); } From 3f808c1dd1abd00a8256b16a4f8cef6e8010f37d Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 27 Aug 2021 14:44:39 +0200 Subject: [PATCH 101/123] [in_app_purchase] Ensure purchases correctly report if they are acknowledged on Android (#4257) * Ensure purchases correctly show they are acknowledged * Update packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md Co-authored-by: Rene Floor * Update packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md Co-authored-by: Rene Floor * Modify so public API is not changed Co-authored-by: Rene Floor --- .../in_app_purchase_android/CHANGELOG.md | 6 ++++- .../types/google_play_purchase_details.dart | 25 ++++++------------- .../in_app_purchase_android/pubspec.yaml | 2 +- .../purchase_wrapper_test.dart | 22 +++++++++++++++- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index 8e342a65422c..1a03ba27feb7 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.4+6 + +* Ensure that purchases correctly indicate whether they are acknowledged or not. The `PurchaseDetails.pendingCompletePurchase` field now correctly indicates if the purchase still needs to be completed. + ## 0.1.4+5 * Add `implements` to pubspec. @@ -9,7 +13,7 @@ ## 0.1.4+3 -- Updated installation instructions in README. +* Updated installation instructions in README. ## 0.1.4+2 diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart index 66e3a8f5a590..53b58bd664fd 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart @@ -20,30 +20,19 @@ class GooglePlayPurchaseDetails extends PurchaseDetails { required this.billingClientPurchase, required PurchaseStatus status, }) : super( - productID: productID, - purchaseID: purchaseID, - transactionDate: transactionDate, - verificationData: verificationData, - status: status) { - this.status = status; + productID: productID, + purchaseID: purchaseID, + transactionDate: transactionDate, + verificationData: verificationData, + status: status, + ) { + this.pendingCompletePurchase = !billingClientPurchase.isAcknowledged; } /// Points back to the [PurchaseWrapper] which was used to generate this /// [GooglePlayPurchaseDetails] object. final PurchaseWrapper billingClientPurchase; - late PurchaseStatus _status; - - /// The status that this [PurchaseDetails] is currently on. - PurchaseStatus get status => _status; - set status(PurchaseStatus status) { - _pendingCompletePurchase = status == PurchaseStatus.purchased; - _status = status; - } - - bool _pendingCompletePurchase = false; - bool get pendingCompletePurchase => _pendingCompletePurchase; - /// Generate a [PurchaseDetails] object based on an Android [Purchase] object. factory GooglePlayPurchaseDetails.fromPurchase(PurchaseWrapper purchase) { final GooglePlayPurchaseDetails purchaseDetails = GooglePlayPurchaseDetails( diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml index 745b651e5828..d9b09827824b 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.4+5 +version: 0.1.4+6 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart index bb7ff8535c7a..70b9fcad4da7 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart @@ -71,9 +71,10 @@ void main() { expect(parsed, equals(expected)); }); - test('toPurchaseDetails() should return correct PurchaseDetail object', () { + test('fromPurchase() should return correct PurchaseDetail object', () { final GooglePlayPurchaseDetails details = GooglePlayPurchaseDetails.fromPurchase(dummyPurchase); + expect(details.purchaseID, dummyPurchase.orderId); expect(details.productID, dummyPurchase.sku); expect(details.transactionDate, dummyPurchase.purchaseTime.toString()); @@ -84,6 +85,25 @@ void main() { expect(details.verificationData.serverVerificationData, dummyPurchase.purchaseToken); expect(details.billingClientPurchase, dummyPurchase); + expect(details.pendingCompletePurchase, false); + }); + + test( + 'fromPurchase() should return set pendingCompletePurchase to true for unacknowledged purchase', + () { + final GooglePlayPurchaseDetails details = + GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase); + + expect(details.purchaseID, dummyPurchase.orderId); + expect(details.productID, dummyPurchase.sku); + expect(details.transactionDate, dummyPurchase.purchaseTime.toString()); + expect(details.verificationData, isNotNull); + expect(details.verificationData.source, kIAPSource); + expect(details.verificationData.localVerificationData, + dummyPurchase.originalJson); + expect(details.verificationData.serverVerificationData, + dummyPurchase.purchaseToken); + expect(details.billingClientPurchase, dummyUnacknowledgedPurchase); expect(details.pendingCompletePurchase, true); }); }); From 0588bfea1d5ce41bf84adcd798d234758149850d Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 27 Aug 2021 11:36:03 -0400 Subject: [PATCH 102/123] Remove support for bypassing, or prompting for, git tagging (#4275) We never want a plugin to be published without tagging the release, so there's no reason to support the added complexity of these flags. Similarly, once someone has confirmed publishing, we don't want to give them an opt-out for doing the tag. --- script/tool/CHANGELOG.md | 2 + .../tool/lib/src/publish_plugin_command.dart | 69 +++----- .../test/publish_plugin_command_test.dart | 150 ++++-------------- script/tool/test/util.dart | 3 +- 4 files changed, 51 insertions(+), 173 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index a32fb0016cb3..b10237b45913 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -5,6 +5,8 @@ - Pubspec validation now checks for `implements` in implementation packages. - Pubspec valitation now checks the full relative path of `repository` entries. - `build-examples` now supports UWP plugins via a `--winuwp` flag. +- **Breaking change**: `publish` no longer accepts `--no-tag-release` or + `--no-push-flags`. Releases now always tag and push. ## 0.5.0 diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index be9e6d300125..8432e342cda3 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -66,23 +66,9 @@ class PublishPluginCommand extends PluginCommand { argParser.addMultiOption(_pubFlagsOption, help: 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); - argParser.addFlag( - _tagReleaseOption, - help: 'Whether or not to tag the release.', - defaultsTo: true, - negatable: true, - ); - argParser.addFlag( - _pushTagsOption, - help: - 'Whether or not tags should be pushed to a remote after creation. Ignored if tag-release is false.', - defaultsTo: true, - negatable: true, - ); argParser.addOption( _remoteOption, - help: - 'The name of the remote to push the tags to. Ignored if push-tags or tag-release is false.', + help: 'The name of the remote to push the tags to.', // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. defaultsTo: 'upstream', ); @@ -104,15 +90,12 @@ class PublishPluginCommand extends PluginCommand { ); argParser.addFlag(_skipConfirmationFlag, help: 'Run the command without asking for Y/N inputs.\n' - 'This command will add a `--force` flag to the `pub publish` command if it is not added with $_pubFlagsOption\n' - 'It also skips the y/n inputs when pushing tags to remote.\n', + 'This command will add a `--force` flag to the `pub publish` command if it is not added with $_pubFlagsOption\n', defaultsTo: false, negatable: true); } static const String _packageOption = 'package'; - static const String _tagReleaseOption = 'tag-release'; - static const String _pushTagsOption = 'push-tags'; static const String _pubFlagsOption = 'pub-publish-flags'; static const String _remoteOption = 'remote'; static const String _allChangedFlag = 'all-changed'; @@ -150,19 +133,14 @@ class PublishPluginCommand extends PluginCommand { print('Checking local repo...'); final GitDir repository = await gitDir; - - final bool shouldPushTag = getBoolArg(_pushTagsOption); - _RemoteInfo? remote; - if (shouldPushTag) { - final String remoteName = getStringArg(_remoteOption); - final String? remoteUrl = await _verifyRemote(remoteName); - if (remoteUrl == null) { - printError( - 'Unable to find URL for remote $remoteName; cannot push tags'); - throw ToolExit(1); - } - remote = _RemoteInfo(name: remoteName, url: remoteUrl); + final String remoteName = getStringArg(_remoteOption); + final String? remoteUrl = await _verifyRemote(remoteName); + if (remoteUrl == null) { + printError('Unable to find URL for remote $remoteName; cannot push tags'); + throw ToolExit(1); } + final _RemoteInfo remote = _RemoteInfo(name: remoteName, url: remoteUrl); + print('Local repo is ready!'); if (getBoolArg(_dryRunFlag)) { print('=============== DRY RUN ==============='); @@ -187,7 +165,7 @@ class PublishPluginCommand extends PluginCommand { Future _publishAllChangedPackages({ required GitDir baseGitDir, - _RemoteInfo? remoteForTagPush, + required _RemoteInfo remoteForTagPush, }) async { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); final List changedPubspecs = @@ -249,24 +227,21 @@ class PublishPluginCommand extends PluginCommand { return packagesFailed.isEmpty; } - // Publish the package to pub with `pub publish`. - // If `_tagReleaseOption` is on, git tag the release. - // If `remoteForTagPush` is non-null, the tag will be pushed to that remote. + // Publish the package to pub with `pub publish`, then git tag the release + // and push the tag to [remoteForTagPush]. // Returns `true` if publishing and tagging are successful. Future _publishAndTagPackage({ required Directory packageDir, - _RemoteInfo? remoteForTagPush, + required _RemoteInfo remoteForTagPush, }) async { if (!await _publishPlugin(packageDir: packageDir)) { return false; } - if (getBoolArg(_tagReleaseOption)) { - if (!await _tagRelease( - packageDir: packageDir, - remoteForPush: remoteForTagPush, - )) { - return false; - } + if (!await _tagRelease( + packageDir: packageDir, + remoteForPush: remoteForTagPush, + )) { + return false; } print('Released [${packageDir.basename}] successfully.'); return true; @@ -479,14 +454,6 @@ Safe to ignore if the package is deleted in this commit. required _RemoteInfo remote, }) async { assert(remote != null && tag != null); - if (!getBoolArg(_skipConfirmationFlag)) { - print('Ready to push $tag to ${remote.url} (y/n)?'); - final String? input = _stdin.readLineSync(); - if (input?.toLowerCase() != 'y') { - print('Tag push canceled.'); - return false; - } - } if (!getBoolArg(_dryRunFlag)) { final io.ProcessResult result = await (await gitDir).runCommand( ['push', remote.name, tag], diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 663c2633a9db..927c146a874d 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -80,8 +80,8 @@ void main() { test('requires an existing flag', () async { Error? commandError; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--package', 'iamerror', '--no-push-tags'], + final List output = await runCapturingPrint( + commandRunner, ['publish-plugin', '--package', 'iamerror'], errorHandler: (Error e) { commandError = e; }); @@ -100,8 +100,8 @@ void main() { ]; Error? commandError; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--package', 'foo', '--no-push-tags'], + final List output = await runCapturingPrint( + commandRunner, ['publish-plugin', '--package', 'foo'], errorHandler: (Error e) { commandError = e; }); @@ -141,56 +141,6 @@ void main() { 'Unable to find URL for remote upstream; cannot push tags'), ])); }); - - test("doesn't validate the remote if it's not pushing tags", () async { - createFakePlugin('foo', packagesDir, examples: []); - - // Checking the remote should fail. - processRunner.mockProcessesForExecutable['git-remote'] = [ - MockProcess(exitCode: 1), - ]; - - final List output = await runCapturingPrint( - commandRunner, [ - 'publish-plugin', - '--package', - 'foo', - '--no-push-tags', - '--no-tag-release' - ]); - - expect( - output, - containsAllInOrder([ - contains('Running `pub publish ` in /packages/foo...'), - contains('Package published!'), - contains('Released [foo] successfully.'), - ])); - }); - - test('can publish non-flutter package', () async { - const String packageName = 'a_package'; - createFakePackage(packageName, packagesDir); - - final List output = await runCapturingPrint( - commandRunner, [ - 'publish-plugin', - '--package', - packageName, - '--no-push-tags', - '--no-tag-release' - ]); - - expect( - output, - containsAllInOrder( - [ - contains('Running `pub publish ` in /packages/a_package...'), - contains('Package published!'), - ], - ), - ); - }); }); group('Publishes package', () { @@ -206,13 +156,7 @@ void main() { ]; final List output = await runCapturingPrint( - commandRunner, [ - 'publish-plugin', - '--package', - 'foo', - '--no-push-tags', - '--no-tag-release' - ]); + commandRunner, ['publish-plugin', '--package', 'foo']); expect( output, @@ -227,13 +171,8 @@ void main() { mockStdin.mockUserInputs.add(utf8.encode('user input')); - await runCapturingPrint(commandRunner, [ - 'publish-plugin', - '--package', - 'foo', - '--no-push-tags', - '--no-tag-release' - ]); + await runCapturingPrint( + commandRunner, ['publish-plugin', '--package', 'foo']); expect(processRunner.mockPublishProcess.stdinMock.lines, contains('user input')); @@ -247,8 +186,6 @@ void main() { 'publish-plugin', '--package', 'foo', - '--no-push-tags', - '--no-tag-release', '--pub-publish-flags', '--dry-run,--server=foo' ]); @@ -272,8 +209,6 @@ void main() { 'publish-plugin', '--package', 'foo', - '--no-push-tags', - '--no-tag-release', '--skip-confirmation', '--pub-publish-flags', '--server=foo' @@ -300,8 +235,6 @@ void main() { 'publish-plugin', '--package', 'foo', - '--no-push-tags', - '--no-tag-release', ], errorHandler: (Error e) { commandError = e; }); @@ -324,8 +257,6 @@ void main() { '--package', 'foo', '--dry-run', - '--no-push-tags', - '--no-tag-release', ]); expect( @@ -340,6 +271,28 @@ void main() { 'Done!' ])); }); + + test('can publish non-flutter package', () async { + const String packageName = 'a_package'; + createFakePackage(packageName, packagesDir); + + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--package', + packageName, + ]); + + expect( + output, + containsAllInOrder( + [ + contains('Running `pub publish ` in /packages/a_package...'), + contains('Package published!'), + ], + ), + ); + }); }); group('Tags release', () { @@ -349,7 +302,6 @@ void main() { 'publish-plugin', '--package', 'foo', - '--no-push-tags', ]); expect(processRunner.recordedCalls, @@ -369,7 +321,6 @@ void main() { 'publish-plugin', '--package', 'foo', - '--no-push-tags', ], errorHandler: (Error e) { commandError = e; }); @@ -388,25 +339,6 @@ void main() { }); group('Pushes tags', () { - test('requires user confirmation', () async { - createFakePlugin('foo', packagesDir, examples: []); - - mockStdin.readLineOutput = 'help'; - - Error? commandError; - final List output = - await runCapturingPrint(commandRunner, [ - 'publish-plugin', - '--package', - 'foo', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect(output, contains('Tag push canceled.')); - }); - test('to upstream by default', () async { createFakePlugin('foo', packagesDir, examples: []); @@ -502,30 +434,6 @@ void main() { contains('Released [foo] successfully.'), ])); }); - - test('only if tagging and pushing to remotes are both enabled', () async { - createFakePlugin('foo', packagesDir, examples: []); - - final List output = - await runCapturingPrint(commandRunner, [ - 'publish-plugin', - '--package', - 'foo', - '--no-tag-release', - ]); - - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - expect( - output, - containsAllInOrder([ - contains('Running `pub publish ` in /packages/foo...'), - contains('Package published!'), - contains('Released [foo] successfully.'), - ])); - }); }); group('Auto release (all-changed flag)', () { diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 9b92a5d94ac8..74c036489233 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -107,7 +107,8 @@ Directory createFakePackage( final Directory packageDirectory = parentDirectory.childDirectory(name); packageDirectory.createSync(recursive: true); - createFakePubspec(packageDirectory, name: name, isFlutter: isFlutter); + createFakePubspec(packageDirectory, + name: name, isFlutter: isFlutter, version: version); createFakeCHANGELOG(packageDirectory, ''' ## $version * Some changes. From 797c61d6b613068326997ac2456dff70af41a3ec Mon Sep 17 00:00:00 2001 From: BeMacized Date: Fri, 27 Aug 2021 17:41:03 +0200 Subject: [PATCH 103/123] [camera] Fix a disposed camera controller throwing an exception when being replaced in the preview widget. (#4272) --- packages/camera/camera/CHANGELOG.md | 4 ++++ packages/camera/camera/example/lib/main.dart | 10 +++------- packages/camera/camera/lib/src/camera_controller.dart | 10 ++++++++++ packages/camera/camera/pubspec.yaml | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index bb0048036f58..5a3a1bf251d7 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.2+1 + +* Fixed camera controller throwing an exception when being replaced in the preview widget. + ## 0.9.2 * Added functions to pause and resume the camera preview. diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 364f59d81356..a8067001aae5 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -603,7 +603,9 @@ class _CameraExampleHomeState extends State } void onNewCameraSelected(CameraDescription cameraDescription) async { - final previousCameraController = controller; + if (controller != null) { + await controller!.dispose(); + } final CameraController cameraController = CameraController( cameraDescription, @@ -614,10 +616,6 @@ class _CameraExampleHomeState extends State controller = cameraController; - if (mounted) { - setState(() {}); - } - // If the controller is updated then update the UI. cameraController.addListener(() { if (mounted) setState(() {}); @@ -650,8 +648,6 @@ class _CameraExampleHomeState extends State if (mounted) { setState(() {}); } - - await previousCameraController?.dispose(); } void onTakePictureButtonPressed() { diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 58193bd204c4..f21a3b12c81f 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -824,4 +824,14 @@ class CameraController extends ValueNotifier { ); } } + + @override + void removeListener(VoidCallback listener) { + // Prevent ValueListenableBuilder in CameraPreview widget from causing an + // exception to be thrown by attempting to remove its own listener after + // the controller has already been disposed. + if (!_isDisposed) { + super.removeListener(listener); + } + } } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 3e3fad15051b..400b8c03f44a 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.2 +version: 0.9.2+1 environment: sdk: ">=2.12.0 <3.0.0" From 78b914d4556d839598ce196efaabb1e4a44d6384 Mon Sep 17 00:00:00 2001 From: Casey Hillers Date: Fri, 27 Aug 2021 10:26:03 -0700 Subject: [PATCH 104/123] [ci.yaml] Add linux platform properties (#4282) --- .ci.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.ci.yaml b/.ci.yaml index 86bc72c7aebf..1205c1ac104d 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -9,6 +9,17 @@ enabled_branches: - master platform_properties: + linux: + properties: + caches: >- + [ + ] + dependencies: > + [ + {"dependency": "curl"} + ] + device_type: none + os: Linux windows: properties: caches: >- From 83f8c4c6a435ab543537875afbb7d5c2e0f6a4dc Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 27 Aug 2021 11:22:10 -0700 Subject: [PATCH 105/123] [ci] update wait-on-check version and set verbose to false (#4262) --- .github/workflows/release.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f6753e5a2add..d3418683fde2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,15 +28,17 @@ jobs: run: dart pub get working-directory: ${{ github.workspace }}/script/tool - # # This workflow should be the last to run. So wait for all the other tests to succeed. + # This workflow should be the last to run. So wait for all the other tests to succeed. - name: Wait on all tests - uses: lewagon/wait-on-check-action@1b1630e169116b58a4b933d5ad7effc46d3d312d + uses: lewagon/wait-on-check-action@0179dfc359f90a703c41240506f998ee1603f9ea with: ref: ${{ github.sha }} running-workflow-name: 'release' repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 180 # seconds allowed-conclusions: success + # verbose:true will produce too many logs that hang github actions web UI. + verbose: false - name: run release run: | From 8d4be08b6f5f50444e8f0415736f9fb97f1adf97 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 27 Aug 2021 11:50:11 -0700 Subject: [PATCH 106/123] [ci] Fix wrong hash used in release.yml (#4286) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3418683fde2..7f1a4a360949 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: # This workflow should be the last to run. So wait for all the other tests to succeed. - name: Wait on all tests - uses: lewagon/wait-on-check-action@0179dfc359f90a703c41240506f998ee1603f9ea + uses: lewagon/wait-on-check-action@a0f99ce1e713de216866868c3da4d4183a051cbe with: ref: ${{ github.sha }} running-workflow-name: 'release' From 4f4a88900b772a83e5f7111a05be9132ec17d65e Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 27 Aug 2021 12:18:06 -0700 Subject: [PATCH 107/123] [ci] Revert the wait-on-check hash change (#4287) --- .github/workflows/release.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f1a4a360949..00fa140b131a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,15 +30,13 @@ jobs: # This workflow should be the last to run. So wait for all the other tests to succeed. - name: Wait on all tests - uses: lewagon/wait-on-check-action@a0f99ce1e713de216866868c3da4d4183a051cbe + uses: lewagon/wait-on-check-action@1b1630e169116b58a4b933d5ad7effc46d3d312d with: ref: ${{ github.sha }} running-workflow-name: 'release' repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 180 # seconds allowed-conclusions: success - # verbose:true will produce too many logs that hang github actions web UI. - verbose: false - name: run release run: | From 9b614eaa8394de04bbd530bcba8a585b1ded4cab Mon Sep 17 00:00:00 2001 From: Casey Hillers Date: Fri, 27 Aug 2021 12:21:05 -0700 Subject: [PATCH 108/123] [ci.yaml] Add roller to presubmit (#4283) --- .ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.ci.yaml b/.ci.yaml index 1205c1ac104d..6b5c385aa98e 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -58,7 +58,6 @@ targets: - name: Linux ci_yaml plugins roller recipe: infra/ci_yaml - bringup: true timeout: 30 scheduler: luci runIf: From d9f90b5d4b92d764c04352a2f5f21a441199f184 Mon Sep 17 00:00:00 2001 From: Konstantin Scheglov Date: Fri, 27 Aug 2021 14:11:03 -0600 Subject: [PATCH 109/123] Fix UNNECESSARY_TYPE_CHECK_TRUE. (#4284) --- .../sku_details_wrapper.dart | 45 +++++++++---------- .../test/fakes/fake_ios_platform.dart | 1 - .../sk_methodchannel_apis_test.dart | 3 -- 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart index da4d5c73d851..5bbe7504783d 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart @@ -127,22 +127,21 @@ class SkuDetailsWrapper { return false; } - final SkuDetailsWrapper typedOther = other; - return typedOther is SkuDetailsWrapper && - typedOther.description == description && - typedOther.freeTrialPeriod == freeTrialPeriod && - typedOther.introductoryPrice == introductoryPrice && - typedOther.introductoryPriceMicros == introductoryPriceMicros && - typedOther.introductoryPriceCycles == introductoryPriceCycles && - typedOther.introductoryPricePeriod == introductoryPricePeriod && - typedOther.price == price && - typedOther.priceAmountMicros == priceAmountMicros && - typedOther.sku == sku && - typedOther.subscriptionPeriod == subscriptionPeriod && - typedOther.title == title && - typedOther.type == type && - typedOther.originalPrice == originalPrice && - typedOther.originalPriceAmountMicros == originalPriceAmountMicros; + return other is SkuDetailsWrapper && + other.description == description && + other.freeTrialPeriod == freeTrialPeriod && + other.introductoryPrice == introductoryPrice && + other.introductoryPriceMicros == introductoryPriceMicros && + other.introductoryPriceCycles == introductoryPriceCycles && + other.introductoryPricePeriod == introductoryPricePeriod && + other.price == price && + other.priceAmountMicros == priceAmountMicros && + other.sku == sku && + other.subscriptionPeriod == subscriptionPeriod && + other.title == title && + other.type == type && + other.originalPrice == originalPrice && + other.originalPriceAmountMicros == originalPriceAmountMicros; } @override @@ -195,10 +194,9 @@ class SkuDetailsResponseWrapper { return false; } - final SkuDetailsResponseWrapper typedOther = other; - return typedOther is SkuDetailsResponseWrapper && - typedOther.billingResult == billingResult && - typedOther.skuDetailsList == skuDetailsList; + return other is SkuDetailsResponseWrapper && + other.billingResult == billingResult && + other.skuDetailsList == skuDetailsList; } @override @@ -240,10 +238,9 @@ class BillingResultWrapper { return false; } - final BillingResultWrapper typedOther = other; - return typedOther is BillingResultWrapper && - typedOther.responseCode == responseCode && - typedOther.debugMessage == debugMessage; + return other is BillingResultWrapper && + other.responseCode == responseCode && + other.debugMessage == debugMessage; } @override diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart index 9797dba59684..e7dbd1a49ae2 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart @@ -117,7 +117,6 @@ class FakeIOSPlatform { } List productIDS = List.castFrom(call.arguments); - assert(productIDS is List, 'invalid argument type'); List invalidFound = []; List products = []; for (String productID in productIDS) { diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart index 892b9d346ada..c7f7d800f45f 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart @@ -219,9 +219,6 @@ class FakeIOSPlatform { switch (call.method) { // request makers case '-[InAppPurchasePlugin startProductRequest:result:]': - List productIDS = - List.castFrom(call.arguments); - assert(productIDS is List, 'invalid argument type'); startProductRequestParam = call.arguments; if (getProductRequestFailTest) { return Future.value(null); From 39c1880eee2de00e458ff9d16b266c987eac9f8f Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 27 Aug 2021 23:01:05 +0200 Subject: [PATCH 110/123] [camera_web] Add an initial device orientation event (#4278) --- .../integration_test/camera_web_test.dart | 38 ++++++++++++++++++- .../camera/camera_web/lib/src/camera_web.dart | 6 ++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 9ab8c511f753..4c1d96983fc7 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -2269,6 +2269,40 @@ void main() { }); }); + testWidgets('emits the initial DeviceOrientationChangedEvent', + (tester) async { + when( + () => cameraService.mapOrientationTypeToDeviceOrientation( + OrientationType.portraitPrimary, + ), + ).thenReturn(DeviceOrientation.portraitUp); + + // Set the initial screen orientation to portraitPrimary. + when(() => screenOrientation.type) + .thenReturn(OrientationType.portraitPrimary); + + final eventStreamController = StreamController(); + + when(() => screenOrientation.onChange) + .thenAnswer((_) => eventStreamController.stream); + + final Stream eventStream = + CameraPlatform.instance.onDeviceOrientationChanged(); + + final streamQueue = StreamQueue(eventStream); + + expect( + await streamQueue.next, + equals( + DeviceOrientationChangedEvent( + DeviceOrientation.portraitUp, + ), + ), + ); + + await streamQueue.cancel(); + }); + testWidgets( 'emits a DeviceOrientationChangedEvent ' 'when the screen orientation is changed', (tester) async { @@ -2299,7 +2333,7 @@ void main() { when(() => screenOrientation.type) .thenReturn(OrientationType.landscapePrimary); - eventStreamController.add(Event('orientationChanged')); + eventStreamController.add(Event('change')); expect( await streamQueue.next, @@ -2315,7 +2349,7 @@ void main() { when(() => screenOrientation.type) .thenReturn(OrientationType.portraitSecondary); - eventStreamController.add(Event('orientationChanged')); + eventStreamController.add(Event('change')); expect( await streamQueue.next, diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 19ee43f36660..9d349a788558 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -343,7 +343,11 @@ class CameraPlugin extends CameraPlatform { final orientation = window?.screen?.orientation; if (orientation != null) { - return orientation.onChange.map( + // Create an initial orientation event that emits the device orientation + // as soon as subscribed to this stream. + final initialOrientationEvent = html.Event("change"); + + return orientation.onChange.startWith(initialOrientationEvent).map( (html.Event _) { final deviceOrientation = _cameraService .mapOrientationTypeToDeviceOrientation(orientation.type!); From 863d088f9908e6174aab257dd0790200285ce02d Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 27 Aug 2021 23:05:41 +0200 Subject: [PATCH 111/123] [camera_web] Update ultra high resolution to 4096x2160 (#4279) * feat: update ultra high resolution to 4096x2160 * test: update max and ultra high resolution tests --- .../example/integration_test/camera_service_test.dart | 8 ++++---- packages/camera/camera_web/lib/src/camera_service.dart | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_service_test.dart b/packages/camera/camera_web/example/integration_test/camera_service_test.dart index 937f023f4b36..346ab26237ea 100644 --- a/packages/camera/camera_web/example/integration_test/camera_service_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_service_test.dart @@ -696,20 +696,20 @@ void main() { group('mapResolutionPresetToSize', () { testWidgets( - 'returns 3840x2160 ' + 'returns 4096x2160 ' 'when the resolution preset is max', (tester) async { expect( cameraService.mapResolutionPresetToSize(ResolutionPreset.max), - equals(Size(3840, 2160)), + equals(Size(4096, 2160)), ); }); testWidgets( - 'returns 3840x2160 ' + 'returns 4096x2160 ' 'when the resolution preset is ultraHigh', (tester) async { expect( cameraService.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), - equals(Size(3840, 2160)), + equals(Size(4096, 2160)), ); }); diff --git a/packages/camera/camera_web/lib/src/camera_service.dart b/packages/camera/camera_web/lib/src/camera_service.dart index 612b2b138fdb..5ba5c80395cc 100644 --- a/packages/camera/camera_web/lib/src/camera_service.dart +++ b/packages/camera/camera_web/lib/src/camera_service.dart @@ -277,7 +277,7 @@ class CameraService { switch (resolutionPreset) { case ResolutionPreset.max: case ResolutionPreset.ultraHigh: - return Size(3840, 2160); + return Size(4096, 2160); case ResolutionPreset.veryHigh: return Size(1920, 1080); case ResolutionPreset.high: From e07f161f8679d4cf20ae44c6e006317f29b39b4b Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 27 Aug 2021 23:05:52 +0200 Subject: [PATCH 112/123] [camera_web] Mute the camera preview (#4280) * feat: mute the camera preview * test: update camera video element test with muted set to true --- .../camera/camera_web/example/integration_test/camera_test.dart | 2 +- packages/camera/camera_web/lib/src/camera.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index f331cc1485ab..34142d146a56 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -98,7 +98,7 @@ void main() { expect(camera.videoElement, isNotNull); expect(camera.videoElement.autoplay, isFalse); - expect(camera.videoElement.muted, !audioConstraints.enabled); + expect(camera.videoElement.muted, isTrue); expect(camera.videoElement.srcObject, mediaStream); expect(camera.videoElement.attributes.keys, contains('playsinline')); diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 74d8546fbb12..b7f55c6dea2e 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -119,7 +119,7 @@ class Camera { videoElement ..autoplay = false - ..muted = !options.audio.enabled + ..muted = true ..srcObject = stream ..setAttribute('playsinline', ''); From e02b647ea035b855389cdca0b8f4dca48f4dfdc7 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sat, 28 Aug 2021 12:39:15 -0400 Subject: [PATCH 113/123] [flutter_plugin_tools] Switch 'publish' from --package to --packages (#4285) Replaces the `publish`-command-specific `--package` flag with support for `--packages`, and unifies the flow with the existing looping for `--all-changed`. This better aligns the command's API with the rest of the commands, and reduces divergence in the two flows (e.g., `--package` would attempt to publish and fail if the package was already published, whereas now using `--packages` will use the flow that pre-checks against `pub.dev`). It also sets up a structure that will allow easily converting it to the new base package looping command that most other commands now use, which will be done in a follow-up. Since all calls now attempt to contact `pub.dev`, the tests have been adjusted to always mock the HTTP client so they will be hermetic. Part of https://github.com/flutter/flutter/issues/83413 --- script/tool/CHANGELOG.md | 2 + .../tool/lib/src/publish_plugin_command.dart | 171 +++++---- .../test/publish_plugin_command_test.dart | 334 +++++------------- 3 files changed, 168 insertions(+), 339 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index b10237b45913..634360461c8d 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -7,6 +7,8 @@ - `build-examples` now supports UWP plugins via a `--winuwp` flag. - **Breaking change**: `publish` no longer accepts `--no-tag-release` or `--no-push-flags`. Releases now always tag and push. +- **Breaking change**: `publish`'s `--package` flag has been replaced with the + `--packages` flag used by most other packages. ## 0.5.0 diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 8432e342cda3..aafe7868d8d0 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'dart:io' as io; import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:git/git.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; @@ -58,11 +59,6 @@ class PublishPluginCommand extends PluginCommand { _stdin = stdinput ?? io.stdin, super(packagesDir, platform: platform, processRunner: processRunner, gitDir: gitDir) { - argParser.addOption( - _packageOption, - help: 'The package to publish.' - 'If the package directory name is different than its pubspec.yaml name, then this should specify the directory.', - ); argParser.addMultiOption(_pubFlagsOption, help: 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); @@ -75,8 +71,8 @@ class PublishPluginCommand extends PluginCommand { argParser.addFlag( _allChangedFlag, help: - 'Release all plugins that contains pubspec changes at the current commit compares to the base-sha.\n' - 'The $_packageOption option is ignored if this is on.', + 'Release all packages that contains pubspec changes at the current commit compares to the base-sha.\n' + 'The --packages option is ignored if this is on.', defaultsTo: false, ); argParser.addFlag( @@ -95,7 +91,6 @@ class PublishPluginCommand extends PluginCommand { negatable: true); } - static const String _packageOption = 'package'; static const String _pubFlagsOption = 'pub-publish-flags'; static const String _remoteOption = 'remote'; static const String _allChangedFlag = 'all-changed'; @@ -113,7 +108,7 @@ class PublishPluginCommand extends PluginCommand { @override final String description = - 'Attempts to publish the given plugin and tag its release on GitHub.\n' + 'Attempts to publish the given packages and tag the release(s) on GitHub.\n' 'If running this on CI, an environment variable named $_pubCredentialName must be set to a String that represents the pub credential JSON.\n' 'WARNING: Do not check in the content of pub credential JSON, it should only come from secure sources.'; @@ -123,14 +118,6 @@ class PublishPluginCommand extends PluginCommand { @override Future run() async { - final String packageName = getStringArg(_packageOption); - final bool publishAllChanged = getBoolArg(_allChangedFlag); - if (packageName.isEmpty && !publishAllChanged) { - printError( - 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); - throw ToolExit(1); - } - print('Checking local repo...'); final GitDir repository = await gitDir; final String remoteName = getStringArg(_remoteOption); @@ -146,36 +133,52 @@ class PublishPluginCommand extends PluginCommand { print('=============== DRY RUN ==============='); } - bool successful; - if (publishAllChanged) { - successful = await _publishAllChangedPackages( - baseGitDir: repository, - remoteForTagPush: remote, - ); - } else { - successful = await _publishAndTagPackage( - packageDir: _getPackageDir(packageName), - remoteForTagPush: remote, - ); - } + final List packages = await _getPackagesToProcess() + .where((PackageEnumerationEntry entry) => !entry.excluded) + .toList(); + bool successful = true; + + successful = await _publishPackages( + packages, + baseGitDir: repository, + remoteForTagPush: remote, + ); - _pubVersionFinder.httpClient.close(); await _finish(successful); } - Future _publishAllChangedPackages({ + Stream _getPackagesToProcess() async* { + if (getBoolArg(_allChangedFlag)) { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final List changedPubspecs = + await gitVersionFinder.getChangedPubSpecs(); + + for (final String pubspecPath in changedPubspecs) { + // Convert git's Posix-style paths to a path that matches the current + // filesystem. + final String localStylePubspecPath = + path.joinAll(p.posix.split(pubspecPath)); + final File pubspecFile = packagesDir.fileSystem + .directory((await gitDir).path) + .childFile(localStylePubspecPath); + yield PackageEnumerationEntry(RepositoryPackage(pubspecFile.parent), + excluded: false); + } + } else { + yield* getTargetPackages(filterExcluded: false); + } + } + + Future _publishPackages( + List packages, { required GitDir baseGitDir, required _RemoteInfo remoteForTagPush, }) async { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - final List changedPubspecs = - await gitVersionFinder.getChangedPubSpecs(); - if (changedPubspecs.isEmpty) { + if (packages.isEmpty) { print('No version updates in this commit.'); return true; } - print('Getting existing tags...'); final io.ProcessResult existingTagsResult = await baseGitDir.runCommand(['tag', '--sort=-committerdate']); final List existingTags = (existingTagsResult.stdout as String) @@ -185,16 +188,11 @@ class PublishPluginCommand extends PluginCommand { final List packagesReleased = []; final List packagesFailed = []; - for (final String pubspecPath in changedPubspecs) { - // Convert git's Posix-style paths to a path that matches the current - // filesystem. - final String localStylePubspecPath = - path.joinAll(p.posix.split(pubspecPath)); - final File pubspecFile = packagesDir.fileSystem - .directory(baseGitDir.path) - .childFile(localStylePubspecPath); + for (final PackageEnumerationEntry entry in packages) { + final RepositoryPackage package = entry.package; + final _CheckNeedsReleaseResult result = await _checkNeedsRelease( - pubspecFile: pubspecFile, + package: package, existingTags: existingTags, ); switch (result) { @@ -203,17 +201,15 @@ class PublishPluginCommand extends PluginCommand { case _CheckNeedsReleaseResult.noRelease: continue; case _CheckNeedsReleaseResult.failure: - packagesFailed.add(pubspecFile.parent.basename); + packagesFailed.add(package.displayName); continue; } print('\n'); - if (await _publishAndTagPackage( - packageDir: pubspecFile.parent, - remoteForTagPush: remoteForTagPush, - )) { - packagesReleased.add(pubspecFile.parent.basename); + if (await _publishAndTagPackage(package, + remoteForTagPush: remoteForTagPush)) { + packagesReleased.add(package.displayName); } else { - packagesFailed.add(pubspecFile.parent.basename); + packagesFailed.add(package.displayName); } print('\n'); } @@ -230,31 +226,32 @@ class PublishPluginCommand extends PluginCommand { // Publish the package to pub with `pub publish`, then git tag the release // and push the tag to [remoteForTagPush]. // Returns `true` if publishing and tagging are successful. - Future _publishAndTagPackage({ - required Directory packageDir, - required _RemoteInfo remoteForTagPush, + Future _publishAndTagPackage( + RepositoryPackage package, { + _RemoteInfo? remoteForTagPush, }) async { - if (!await _publishPlugin(packageDir: packageDir)) { + if (!await _publishPackage(package)) { return false; } if (!await _tagRelease( - packageDir: packageDir, + package, remoteForPush: remoteForTagPush, )) { return false; } - print('Released [${packageDir.basename}] successfully.'); + print('Published ${package.directory.basename} successfully.'); return true; } // Returns a [_CheckNeedsReleaseResult] that indicates the result. Future<_CheckNeedsReleaseResult> _checkNeedsRelease({ - required File pubspecFile, + required RepositoryPackage package, required List existingTags, }) async { + final File pubspecFile = package.pubspecFile; if (!pubspecFile.existsSync()) { print(''' -The file at The pubspec file at ${pubspecFile.path} does not exist. Publishing will not happen for ${pubspecFile.parent.basename}. +The pubspec file at ${pubspecFile.path} does not exist. Publishing will not happen for ${pubspecFile.parent.basename}. Safe to ignore if the package is deleted in this commit. '''); return _CheckNeedsReleaseResult.noRelease; @@ -279,7 +276,8 @@ Safe to ignore if the package is deleted in this commit. return _CheckNeedsReleaseResult.failure; } - // Check if the package named `packageName` with `version` has already published. + // Check if the package named `packageName` with `version` has already + // been published. final Version version = pubspec.version!; final PubVersionFinderResponse pubVersionFinderResponse = await _pubVersionFinder.getPackageVersion(packageName: pubspec.name); @@ -303,15 +301,15 @@ Safe to ignore if the package is deleted in this commit. return _CheckNeedsReleaseResult.release; } - // Publish the plugin. + // Publish the package. // // Returns `true` if successful, `false` otherwise. - Future _publishPlugin({required Directory packageDir}) async { - final bool gitStatusOK = await _checkGitStatus(packageDir); + Future _publishPackage(RepositoryPackage package) async { + final bool gitStatusOK = await _checkGitStatus(package); if (!gitStatusOK) { return false; } - final bool publishOK = await _publish(packageDir); + final bool publishOK = await _publish(package); if (!publishOK) { return false; } @@ -319,15 +317,15 @@ Safe to ignore if the package is deleted in this commit. return true; } - // Tag the release with -v, and, if [remoteForTagPush] + // Tag the release with -v, and, if [remoteForTagPush] // is provided, push it to that remote. // // Return `true` if successful, `false` otherwise. - Future _tagRelease({ - required Directory packageDir, + Future _tagRelease( + RepositoryPackage package, { _RemoteInfo? remoteForPush, }) async { - final String tag = _getTag(packageDir); + final String tag = _getTag(package); print('Tagging release $tag...'); if (!getBoolArg(_dryRunFlag)) { final io.ProcessResult result = await (await gitDir).runCommand( @@ -351,6 +349,7 @@ Safe to ignore if the package is deleted in this commit. } Future _finish(bool successful) async { + _pubVersionFinder.httpClient.close(); await _stdinSubscription?.cancel(); _stdinSubscription = null; if (successful) { @@ -361,20 +360,14 @@ Safe to ignore if the package is deleted in this commit. } } - // Returns the packageDirectory based on the package name. - // Throws ToolExit if the `package` doesn't exist. - Directory _getPackageDir(String packageName) { - final Directory packageDir = packagesDir.childDirectory(packageName); - if (!packageDir.existsSync()) { - printError('${packageDir.absolute.path} does not exist.'); - throw ToolExit(1); - } - return packageDir; - } - - Future _checkGitStatus(Directory packageDir) async { + Future _checkGitStatus(RepositoryPackage package) async { final io.ProcessResult statusResult = await (await gitDir).runCommand( - ['status', '--porcelain', '--ignored', packageDir.absolute.path], + [ + 'status', + '--porcelain', + '--ignored', + package.directory.absolute.path + ], throwOnError: false, ); if (statusResult.exitCode != 0) { @@ -402,10 +395,10 @@ Safe to ignore if the package is deleted in this commit. return getRemoteUrlResult.stdout as String?; } - Future _publish(Directory packageDir) async { + Future _publish(RepositoryPackage package) async { final List publishFlags = getStringListArg(_pubFlagsOption); - print( - 'Running `pub publish ${publishFlags.join(' ')}` in ${packageDir.absolute.path}...\n'); + print('Running `pub publish ${publishFlags.join(' ')}` in ' + '${package.directory.absolute.path}...\n'); if (getBoolArg(_dryRunFlag)) { return true; } @@ -419,7 +412,7 @@ Safe to ignore if the package is deleted in this commit. final io.Process publish = await processRunner.start( flutterCommand, ['pub', 'publish'] + publishFlags, - workingDirectory: packageDir); + workingDirectory: package.directory); publish.stdout.transform(utf8.decoder).listen((String data) => print(data)); publish.stderr.transform(utf8.decoder).listen((String data) => print(data)); _stdinSubscription ??= _stdin @@ -427,14 +420,14 @@ Safe to ignore if the package is deleted in this commit. .listen((String data) => publish.stdin.writeln(data)); final int result = await publish.exitCode; if (result != 0) { - printError('Publish ${packageDir.basename} failed.'); + printError('Publishing ${package.directory.basename} failed.'); return false; } return true; } - String _getTag(Directory packageDir) { - final File pubspecFile = packageDir.childFile('pubspec.yaml'); + String _getTag(RepositoryPackage package) { + final File pubspecFile = package.pubspecFile; final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()) as YamlMap; final String name = pubspecYaml['name'] as String; diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 927c146a874d..ae3d768fcc70 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -30,6 +30,8 @@ void main() { late CommandRunner commandRunner; late MockStdin mockStdin; late FileSystem fileSystem; + // Map of package name to mock response. + late Map> mockHttpResponses; void _createMockCredentialFile() { final String credentialPath = PublishPluginCommand.getCredentialPath(); @@ -41,8 +43,20 @@ void main() { setUp(() async { fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = TestProcessRunner(); + + mockHttpResponses = >{}; + final MockClient mockClient = MockClient((http.Request request) async { + final String packageName = + request.url.pathSegments.last.replaceAll('.json', ''); + final Map? response = mockHttpResponses[packageName]; + if (response != null) { + return http.Response(json.encode(response), 200); + } + // Default to simulating the plugin never having been published. + return http.Response('', 404); + }); + gitDir = MockGitDir(); when(gitDir.path).thenReturn(packagesDir.parent.path); when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) @@ -58,39 +72,16 @@ void main() { mockStdin = MockStdin(); commandRunner = CommandRunner('tester', '') - ..addCommand(PublishPluginCommand(packagesDir, - processRunner: processRunner, stdinput: mockStdin, gitDir: gitDir)); + ..addCommand(PublishPluginCommand( + packagesDir, + processRunner: processRunner, + stdinput: mockStdin, + gitDir: gitDir, + httpClient: mockClient, + )); }); group('Initial validation', () { - test('requires a package flag', () async { - Error? commandError; - final List output = await runCapturingPrint( - commandRunner, ['publish-plugin'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Must specify a package to publish.'), - ])); - }); - - test('requires an existing flag', () async { - Error? commandError; - final List output = await runCapturingPrint( - commandRunner, ['publish-plugin', '--package', 'iamerror'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect(output, - containsAllInOrder([contains('iamerror does not exist')])); - }); - test('refuses to proceed with dirty files', () async { final Directory pluginDir = createFakePlugin('foo', packagesDir, examples: []); @@ -100,9 +91,11 @@ void main() { ]; Error? commandError; - final List output = await runCapturingPrint( - commandRunner, ['publish-plugin', '--package', 'foo'], - errorHandler: (Error e) { + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--packages=foo', + ], errorHandler: (Error e) { commandError = e; }); @@ -128,7 +121,7 @@ void main() { Error? commandError; final List output = await runCapturingPrint( - commandRunner, ['publish-plugin', '--package', 'foo'], + commandRunner, ['publish-plugin', '--packages=foo'], errorHandler: (Error e) { commandError = e; }); @@ -145,24 +138,34 @@ void main() { group('Publishes package', () { test('while showing all output from pub publish to the user', () async { - createFakePlugin('foo', packagesDir, examples: []); + createFakePlugin('plugin1', packagesDir, examples: []); + createFakePlugin('plugin2', packagesDir, examples: []); processRunner.mockProcessesForExecutable[flutterCommand] = [ MockProcess( stdout: 'Foo', stderr: 'Bar', stdoutEncoding: utf8, - stderrEncoding: utf8) // pub publish + stderrEncoding: utf8), // pub publish for plugin1 + MockProcess( + stdout: 'Baz', + stdoutEncoding: utf8, + stderrEncoding: utf8), // pub publish for plugin1 ]; - final List output = await runCapturingPrint( - commandRunner, ['publish-plugin', '--package', 'foo']); + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--packages=plugin1,plugin2']); expect( output, containsAllInOrder([ + contains('Running `pub publish ` in /packages/plugin1...'), contains('Foo'), contains('Bar'), + contains('Package published!'), + contains('Running `pub publish ` in /packages/plugin2...'), + contains('Baz'), + contains('Package published!'), ])); }); @@ -172,7 +175,7 @@ void main() { mockStdin.mockUserInputs.add(utf8.encode('user input')); await runCapturingPrint( - commandRunner, ['publish-plugin', '--package', 'foo']); + commandRunner, ['publish-plugin', '--packages=foo']); expect(processRunner.mockPublishProcess.stdinMock.lines, contains('user input')); @@ -184,17 +187,16 @@ void main() { await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', '--pub-publish-flags', - '--dry-run,--server=foo' + '--dry-run,--server=bar' ]); expect( processRunner.recordedCalls, contains(ProcessCall( flutterCommand, - const ['pub', 'publish', '--dry-run', '--server=foo'], + const ['pub', 'publish', '--dry-run', '--server=bar'], pluginDir.path))); }); @@ -207,18 +209,17 @@ void main() { await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', '--skip-confirmation', '--pub-publish-flags', - '--server=foo' + '--server=bar' ]); expect( processRunner.recordedCalls, contains(ProcessCall( flutterCommand, - const ['pub', 'publish', '--server=foo', '--force'], + const ['pub', 'publish', '--server=bar', '--force'], pluginDir.path))); }); @@ -233,8 +234,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', ], errorHandler: (Error e) { commandError = e; }); @@ -243,7 +243,7 @@ void main() { expect( output, containsAllInOrder([ - contains('Publish foo failed.'), + contains('Publishing foo failed.'), ])); }); @@ -254,8 +254,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', '--dry-run', ]); @@ -279,8 +278,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - packageName, + '--packages=$packageName', ]); expect( @@ -300,8 +298,7 @@ void main() { createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', ]); expect(processRunner.recordedCalls, @@ -319,8 +316,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', ], errorHandler: (Error e) { commandError = e; }); @@ -329,7 +325,7 @@ void main() { expect( output, containsAllInOrder([ - contains('Publish foo failed.'), + contains('Publishing foo failed.'), ])); expect( processRunner.recordedCalls, @@ -347,8 +343,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', ]); expect( @@ -358,7 +353,7 @@ void main() { expect( output, containsAllInOrder([ - contains('Released [foo] successfully.'), + contains('Published foo successfully.'), ])); }); @@ -371,8 +366,7 @@ void main() { await runCapturingPrint(commandRunner, [ 'publish-plugin', '--skip-confirmation', - '--package', - 'foo', + '--packages=foo', ]); expect( @@ -382,7 +376,7 @@ void main() { expect( output, containsAllInOrder([ - contains('Released [foo] successfully.'), + contains('Published foo successfully.'), ])); }); @@ -393,7 +387,7 @@ void main() { mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--package', 'foo', '--dry-run']); + ['publish-plugin', '--packages=foo', '--dry-run']); expect( processRunner.recordedCalls @@ -418,8 +412,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', - '--package', - 'foo', + '--packages=foo', '--remote', 'origin', ]); @@ -431,43 +424,23 @@ void main() { expect( output, containsAllInOrder([ - contains('Released [foo] successfully.'), + contains('Published foo successfully.'), ])); }); }); group('Auto release (all-changed flag)', () { test('can release newly created plugins', () async { - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': [], }; - const Map httpResponsePlugin2 = { + mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': [], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'plugin1.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } else if (request.url.pathSegments.last == 'plugin2.json') { - return http.Response(json.encode(httpResponsePlugin2), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); - // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); // federated @@ -492,7 +465,7 @@ void main() { 'Local repo is ready!', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2', + 'Packages released: plugin1, plugin2/plugin2', 'Done!' ])); expect( @@ -507,43 +480,21 @@ void main() { test('can release newly created plugins, while there are existing plugins', () async { - const Map httpResponsePlugin0 = { + mockHttpResponses['plugin0'] = { 'name': 'plugin0', 'versions': ['0.0.1'], }; - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': [], }; - const Map httpResponsePlugin2 = { + mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': [], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'plugin0.json') { - return http.Response(json.encode(httpResponsePlugin0), 200); - } else if (request.url.pathSegments.last == 'plugin1.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } else if (request.url.pathSegments.last == 'plugin2.json') { - return http.Response(json.encode(httpResponsePlugin2), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); - // The existing plugin. createFakePlugin('plugin0', packagesDir); // Non-federated @@ -575,7 +526,7 @@ void main() { 'Local repo is ready!', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2', + 'Packages released: plugin1, plugin2/plugin2', 'Done!' ])); expect( @@ -589,35 +540,16 @@ void main() { }); test('can release newly created plugins, dry run', () async { - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': [], }; - const Map httpResponsePlugin2 = { + mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': [], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'plugin1.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } else if (request.url.pathSegments.last == 'plugin2.json') { - return http.Response(json.encode(httpResponsePlugin2), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); // federated @@ -651,7 +583,7 @@ void main() { 'Running `pub publish ` in ${pluginDir2.path}...\n', 'Tagging release plugin2-v0.0.1...', 'Pushing tag to upstream...', - 'Packages released: plugin1, plugin2', + 'Packages released: plugin1, plugin2/plugin2', 'Done!' ])); expect( @@ -661,36 +593,16 @@ void main() { }); test('version change triggers releases.', () async { - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': ['0.0.1'], }; - const Map httpResponsePlugin2 = { + mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': ['0.0.1'], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'plugin1.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } else if (request.url.pathSegments.last == 'plugin2.json') { - return http.Response(json.encode(httpResponsePlugin2), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); - // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); @@ -716,7 +628,7 @@ void main() { 'Local repo is ready!', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2', + 'Packages released: plugin1, plugin2/plugin2', 'Done!' ])); expect( @@ -732,36 +644,16 @@ void main() { test( 'delete package will not trigger publish but exit the command successfully.', () async { - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': ['0.0.1'], }; - const Map httpResponsePlugin2 = { + mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': ['0.0.1'], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'plugin1.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } else if (request.url.pathSegments.last == 'plugin2.json') { - return http.Response(json.encode(httpResponsePlugin2), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); - // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); @@ -786,7 +678,7 @@ void main() { 'Checking local repo...', 'Local repo is ready!', 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'The file at The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n', + 'The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n', 'Packages released: plugin1', 'Done!' ])); @@ -798,36 +690,16 @@ void main() { test('Existing versions do not trigger release, also prints out message.', () async { - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': ['0.0.2'], }; - const Map httpResponsePlugin2 = { + mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': ['0.0.2'], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'plugin1.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } else if (request.url.pathSegments.last == 'plugin2.json') { - return http.Response(json.encode(httpResponsePlugin2), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); - // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); @@ -871,36 +743,16 @@ void main() { test( 'Existing versions do not trigger release, but fail if the tags do not exist.', () async { - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'plugin1', 'versions': ['0.0.2'], }; - const Map httpResponsePlugin2 = { + mockHttpResponses['plugin2'] = { 'name': 'plugin2', 'versions': ['0.0.2'], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'plugin1.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } else if (request.url.pathSegments.last == 'plugin2.json') { - return http.Response(json.encode(httpResponsePlugin2), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); - // Non-federated final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); @@ -970,29 +822,11 @@ void main() { }); test('Do not release flutter_plugin_tools', () async { - const Map httpResponsePlugin1 = { + mockHttpResponses['plugin1'] = { 'name': 'flutter_plugin_tools', 'versions': [], }; - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'flutter_plugin_tools.json') { - return http.Response(json.encode(httpResponsePlugin1), 200); - } - return http.Response('', 500); - }); - final PublishPluginCommand command = PublishPluginCommand(packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - httpClient: mockClient, - gitDir: gitDir); - - commandRunner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - commandRunner.addCommand(command); - final Directory flutterPluginTools = createFakePlugin('flutter_plugin_tools', packagesDir); processRunner.mockProcessesForExecutable['git-diff'] = [ From 32ca7761cecbd86098856ac503f8cd83eff878f1 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Sat, 28 Aug 2021 10:13:38 -0700 Subject: [PATCH 114/123] Revert "[ci] Revert the wait-on-check hash change (#4287)" (#4289) Relands #4262 as the white list has been updated to allow the newer wait-on-check version. --- .github/workflows/release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00fa140b131a..7f1a4a360949 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,13 +30,15 @@ jobs: # This workflow should be the last to run. So wait for all the other tests to succeed. - name: Wait on all tests - uses: lewagon/wait-on-check-action@1b1630e169116b58a4b933d5ad7effc46d3d312d + uses: lewagon/wait-on-check-action@a0f99ce1e713de216866868c3da4d4183a051cbe with: ref: ${{ github.sha }} running-workflow-name: 'release' repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 180 # seconds allowed-conclusions: success + # verbose:true will produce too many logs that hang github actions web UI. + verbose: false - name: run release run: | From 18129d67395a18b43b18ec0c22dfd8bafa64a826 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Mon, 30 Aug 2021 20:47:40 +0200 Subject: [PATCH 115/123] [camera_web] Do not flip the video on the back camera (#4281) * feat: do not flip the video on the back camera * test: do not flip the video on the back camera tests * feat: update getVideoSize usage --- .../example/integration_test/camera_test.dart | 112 +++++++++++++++++- .../integration_test/camera_web_test.dart | 12 +- .../camera/camera_web/lib/src/camera.dart | 52 ++++++-- .../camera/camera_web/lib/src/camera_web.dart | 2 +- 4 files changed, 158 insertions(+), 20 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 34142d146a56..712d8c77ff3e 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -85,11 +85,17 @@ void main() { 'creates a video element ' 'with correct properties', (tester) async { const audioConstraints = AudioConstraints(enabled: true); + final videoConstraints = VideoConstraints( + facingMode: FacingModeConstraint( + CameraType.user, + ), + ); final camera = Camera( textureId: textureId, options: CameraOptions( audio: audioConstraints, + video: videoConstraints, ), cameraService: cameraService, ); @@ -108,6 +114,27 @@ void main() { expect(camera.videoElement.style.width, equals('100%')); expect(camera.videoElement.style.height, equals('100%')); expect(camera.videoElement.style.objectFit, equals('cover')); + }); + + testWidgets( + 'flips the video element horizontally ' + 'for a back camera', (tester) async { + final videoConstraints = VideoConstraints( + facingMode: FacingModeConstraint( + CameraType.environment, + ), + ); + + final camera = Camera( + textureId: textureId, + options: CameraOptions( + video: videoConstraints, + ), + cameraService: cameraService, + ); + + await camera.initialize(); + expect(camera.videoElement.style.transform, equals('scaleX(-1)')); }); @@ -376,7 +403,7 @@ void main() { await camera.initialize(); expect( - await camera.getVideoSize(), + camera.getVideoSize(), equals(videoSize), ); }); @@ -396,7 +423,7 @@ void main() { await camera.initialize(); expect( - await camera.getVideoSize(), + camera.getVideoSize(), equals(Size.zero), ); }); @@ -819,6 +846,87 @@ void main() { }); }); + group('getLensDirection', () { + testWidgets( + 'returns a lens direction ' + 'based on the first video track settings', (tester) async { + final videoElement = MockVideoElement(); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + )..videoElement = videoElement; + + final firstVideoTrack = MockMediaStreamTrack(); + + when(() => videoElement.srcObject).thenReturn( + FakeMediaStream([ + firstVideoTrack, + MockMediaStreamTrack(), + ]), + ); + + when(firstVideoTrack.getSettings) + .thenReturn({'facingMode': 'environment'}); + + when(() => cameraService.mapFacingModeToLensDirection('environment')) + .thenReturn(CameraLensDirection.external); + + expect( + camera.getLensDirection(), + equals(CameraLensDirection.external), + ); + }); + + testWidgets( + 'returns null ' + 'if the first video track is missing the facing mode', + (tester) async { + final videoElement = MockVideoElement(); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + )..videoElement = videoElement; + + final firstVideoTrack = MockMediaStreamTrack(); + + when(() => videoElement.srcObject).thenReturn( + FakeMediaStream([ + firstVideoTrack, + MockMediaStreamTrack(), + ]), + ); + + when(firstVideoTrack.getSettings).thenReturn({}); + + expect( + camera.getLensDirection(), + isNull, + ); + }); + + testWidgets( + 'returns null ' + 'if the camera is missing video tracks', (tester) async { + // Create a video stream with no video tracks. + final videoElement = VideoElement(); + mediaStream = videoElement.captureStream(); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + + expect( + camera.getLensDirection(), + isNull, + ); + }); + }); + group('getViewType', () { testWidgets('returns a correct view type', (tester) async { final camera = Camera( diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 4c1d96983fc7..4bc10badab05 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -574,9 +574,7 @@ void main() { abortStreamController = StreamController(); endedStreamController = StreamController(); - when(camera.getVideoSize).thenAnswer( - (_) => Future.value(Size(10, 10)), - ); + when(camera.getVideoSize).thenReturn(Size(10, 10)); when(camera.initialize).thenAnswer((_) => Future.value()); when(camera.play).thenAnswer((_) => Future.value()); @@ -1660,9 +1658,7 @@ void main() { abortStreamController = StreamController(); endedStreamController = StreamController(); - when(camera.getVideoSize).thenAnswer( - (_) => Future.value(Size(10, 10)), - ); + when(camera.getVideoSize).thenReturn(Size(10, 10)); when(camera.initialize).thenAnswer((_) => Future.value()); when(camera.play).thenAnswer((_) => Future.value()); when(camera.dispose).thenAnswer((_) => Future.value()); @@ -1818,9 +1814,7 @@ void main() { abortStreamController = StreamController(); endedStreamController = StreamController(); - when(camera.getVideoSize).thenAnswer( - (_) => Future.value(Size(10, 10)), - ); + when(camera.getVideoSize).thenReturn(Size(10, 10)); when(camera.initialize).thenAnswer((_) => Future.value()); when(camera.play).thenAnswer((_) => Future.value()); diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index b7f55c6dea2e..4b7a185b90f7 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -106,7 +106,6 @@ class Camera { ); videoElement = html.VideoElement(); - _applyDefaultVideoStyles(videoElement); divElement = html.DivElement() ..style.setProperty('object-fit', 'cover') @@ -123,6 +122,8 @@ class Camera { ..srcObject = stream ..setAttribute('playsinline', ''); + _applyDefaultVideoStyles(videoElement); + final videoTracks = stream!.getVideoTracks(); if (videoTracks.isNotEmpty) { @@ -149,7 +150,7 @@ class Camera { } /// Pauses the camera stream on the current frame. - void pause() async { + void pause() { videoElement.pause(); } @@ -185,11 +186,17 @@ class Camera { final videoWidth = videoElement.videoWidth; final videoHeight = videoElement.videoHeight; final canvas = html.CanvasElement(width: videoWidth, height: videoHeight); + final isBackCamera = getLensDirection() == CameraLensDirection.back; + + // Flip the picture horizontally if it is not taken from a back camera. + if (!isBackCamera) { + canvas.context2D + ..translate(videoWidth, 0) + ..scale(-1, 1); + } canvas.context2D - ..translate(videoWidth, 0) - ..scale(-1, 1) - ..drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight); + .drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight); final blob = await canvas.toBlob('image/jpeg'); @@ -204,7 +211,7 @@ class Camera { /// /// Returns [Size.zero] if the camera is missing a video track or /// the video track does not include the width or height setting. - Future getVideoSize() async { + Size getVideoSize() { final videoTracks = videoElement.srcObject?.getVideoTracks() ?? []; if (videoTracks.isEmpty) { @@ -332,6 +339,29 @@ class Camera { }); } + /// Returns a lens direction of this camera. + /// + /// Returns null if the camera is missing a video track or + /// the video track does not include the facing mode setting. + CameraLensDirection? getLensDirection() { + final videoTracks = videoElement.srcObject?.getVideoTracks() ?? []; + + if (videoTracks.isEmpty) { + return null; + } + + final defaultVideoTrack = videoTracks.first; + final defaultVideoTrackSettings = defaultVideoTrack.getSettings(); + + final facingMode = defaultVideoTrackSettings['facingMode']; + + if (facingMode != null) { + return _cameraService.mapFacingModeToLensDirection(facingMode); + } else { + return null; + } + } + /// Returns the registered view type of the camera. String getViewType() => _getViewType(textureId); @@ -354,12 +384,18 @@ class Camera { /// Applies default styles to the video [element]. void _applyDefaultVideoStyles(html.VideoElement element) { + final isBackCamera = getLensDirection() == CameraLensDirection.back; + + // Flip the video horizontally if it is not taken from a back camera. + if (!isBackCamera) { + element.style.transform = 'scaleX(-1)'; + } + element.style ..transformOrigin = 'center' ..pointerEvents = 'none' ..width = '100%' ..height = '100%' - ..objectFit = 'cover' - ..transform = 'scaleX(-1)'; + ..objectFit = 'cover'; } } diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 9d349a788558..5c976b8f8657 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -285,7 +285,7 @@ class CameraPlugin extends CameraPlatform { ); }); - final cameraSize = await camera.getVideoSize(); + final cameraSize = camera.getVideoSize(); cameraEventStreamController.add( CameraInitializedEvent( From 5306c02db66b818fff39dc8a2d4eebd0257185fd Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 30 Aug 2021 14:51:58 -0400 Subject: [PATCH 116/123] [flutter_plugin_tool] Add support for running Windows unit tests (#4276) Implements support for `--windows` in `native-test`, for unit tests only. The structure of the new code has most of the new functionality in a generic utility for running GoogleTest test binaries, so that it can be trivially extended to Linux support in a follow-up once the Linux test PoC has landed. This runs the recently-added `url_launcher_windows` unit test. However, it's not yet run in CI since it needs LUCI bringup; that will be done one this support is in place. Requires new logic to check if a plugin contains native code, and some new test utility plumbing to generate plugins whose pubspecs indicate that they only contain Dart code to test it, to allow filtering filtering out the FFI-based Windows plugins. Part of flutter/flutter#82445 --- script/tool/CHANGELOG.md | 1 + script/tool/lib/src/common/file_utils.dart | 20 ++ script/tool/lib/src/common/plugin_utils.dart | 88 +++++--- script/tool/lib/src/native_test_command.dart | 89 +++++++- .../tool/lib/src/publish_plugin_command.dart | 12 +- script/tool/test/common/file_utils_test.dart | 32 +++ .../tool/test/common/plugin_utils_test.dart | 68 ++++++ .../tool/test/native_test_command_test.dart | 208 +++++++++++++++++- script/tool/test/util.dart | 73 +++--- 9 files changed, 510 insertions(+), 81 deletions(-) create mode 100644 script/tool/lib/src/common/file_utils.dart create mode 100644 script/tool/test/common/file_utils_test.dart diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 634360461c8d..0edb106f099c 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -5,6 +5,7 @@ - Pubspec validation now checks for `implements` in implementation packages. - Pubspec valitation now checks the full relative path of `repository` entries. - `build-examples` now supports UWP plugins via a `--winuwp` flag. +- `native-test` now supports `--windows` for unit tests. - **Breaking change**: `publish` no longer accepts `--no-tag-release` or `--no-push-flags`. Releases now always tag and push. - **Breaking change**: `publish`'s `--package` flag has been replaced with the diff --git a/script/tool/lib/src/common/file_utils.dart b/script/tool/lib/src/common/file_utils.dart new file mode 100644 index 000000000000..3c2f2f18f954 --- /dev/null +++ b/script/tool/lib/src/common/file_utils.dart @@ -0,0 +1,20 @@ +// 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:file/file.dart'; + +/// Returns a [File] created by appending all but the last item in [components] +/// to [base] as subdirectories, then appending the last as a file. +/// +/// Example: +/// childFileWithSubcomponents(rootDir, ['foo', 'bar', 'baz.txt']) +/// creates a File representing /rootDir/foo/bar/baz.txt. +File childFileWithSubcomponents(Directory base, List components) { + Directory dir = base; + final String basename = components.removeLast(); + for (final String directoryName in components) { + dir = dir.childDirectory(directoryName); + } + return dir.childFile(basename); +} diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index 49da67655e91..06af675e71ef 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -17,7 +17,7 @@ enum PlatformSupport { federated, } -/// Returns whether the given [package] is a Flutter [platform] plugin. +/// Returns true if [package] is a Flutter [platform] plugin. /// /// It checks this by looking for the following pattern in the pubspec: /// @@ -30,7 +30,7 @@ enum PlatformSupport { /// implementation in order to return true. bool pluginSupportsPlatform( String platform, - RepositoryPackage package, { + RepositoryPackage plugin, { PlatformSupport? requiredMode, String? variant, }) { @@ -41,32 +41,12 @@ bool pluginSupportsPlatform( platform == kPlatformWindows || platform == kPlatformLinux); try { - final YamlMap pubspecYaml = - loadYaml(package.pubspecFile.readAsStringSync()) as YamlMap; - final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; - if (flutterSection == null) { - return false; - } - final YamlMap? pluginSection = flutterSection['plugin'] as YamlMap?; - if (pluginSection == null) { - return false; - } - final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; - if (platforms == null) { - // Legacy plugin specs are assumed to support iOS and Android. They are - // never federated. - if (requiredMode == PlatformSupport.federated) { - return false; - } - if (!pluginSection.containsKey('platforms')) { - return platform == kPlatformIos || platform == kPlatformAndroid; - } - return false; - } - final YamlMap? platformEntry = platforms[platform] as YamlMap?; + final YamlMap? platformEntry = + _readPlatformPubspecSectionForPlugin(platform, plugin); if (platformEntry == null) { return false; } + // If the platform entry is present, then it supports the platform. Check // for required mode if specified. if (requiredMode != null) { @@ -97,9 +77,67 @@ bool pluginSupportsPlatform( } return true; + } on YamlException { + return false; + } +} + +/// Returns true if [plugin] includes native code for [platform], as opposed to +/// being implemented entirely in Dart. +bool pluginHasNativeCodeForPlatform(String platform, RepositoryPackage plugin) { + if (platform == kPlatformWeb) { + // Web plugins are always Dart-only. + return false; + } + try { + final YamlMap? platformEntry = + _readPlatformPubspecSectionForPlugin(platform, plugin); + if (platformEntry == null) { + return false; + } + // All other platforms currently use pluginClass for indicating the native + // code in the plugin. + final String? pluginClass = platformEntry['pluginClass'] as String?; + // TODO(stuartmorgan): Remove the check for 'none' once none of the plugins + // in the repository use that workaround. See + // https://github.com/flutter/flutter/issues/57497 for context. + return pluginClass != null && pluginClass != 'none'; } on FileSystemException { return false; } on YamlException { return false; } } + +/// Returns the +/// flutter: +/// plugin: +/// platforms: +/// [platform]: +/// section from [plugin]'s pubspec.yaml, or null if either it is not present, +/// or the pubspec couldn't be read. +YamlMap? _readPlatformPubspecSectionForPlugin( + String platform, RepositoryPackage plugin) { + try { + final File pubspecFile = plugin.pubspecFile; + final YamlMap pubspecYaml = + loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; + if (flutterSection == null) { + return null; + } + final YamlMap? pluginSection = flutterSection['plugin'] as YamlMap?; + if (pluginSection == null) { + return null; + } + final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; + if (platforms == null) { + return null; + } + return platforms[platform] as YamlMap?; + } on FileSystemException { + return null; + } on YamlException { + return null; + } +} diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 725cf23a2e9a..5120ad10b872 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -40,6 +40,7 @@ class NativeTestCommand extends PackageLoopingCommand { argParser.addFlag(kPlatformAndroid, help: 'Runs Android tests'); argParser.addFlag(kPlatformIos, help: 'Runs iOS tests'); argParser.addFlag(kPlatformMacos, help: 'Runs macOS tests'); + argParser.addFlag(kPlatformWindows, help: 'Runs Windows tests'); // By default, both unit tests and integration tests are run, but provide // flags to disable one or the other. @@ -80,6 +81,7 @@ this command. kPlatformAndroid: _PlatformDetails('Android', _testAndroid), kPlatformIos: _PlatformDetails('iOS', _testIos), kPlatformMacos: _PlatformDetails('macOS', _testMacOS), + kPlatformWindows: _PlatformDetails('Windows', _testWindows), }; _requestedPlatforms = _platforms.keys .where((String platform) => getBoolArg(platform)) @@ -96,6 +98,11 @@ this command. throw ToolExit(exitInvalidArguments); } + if (getBoolArg(kPlatformWindows) && getBoolArg(_integrationTestFlag)) { + logWarning('This command currently only supports unit tests for Windows. ' + 'See https://github.com/flutter/flutter/issues/70233.'); + } + // iOS-specific run-level state. if (_requestedPlatforms.contains('ios')) { String destination = getStringArg(_iosDestinationFlag); @@ -119,16 +126,20 @@ this command. Future runForPackage(RepositoryPackage package) async { final List testPlatforms = []; for (final String platform in _requestedPlatforms) { - if (pluginSupportsPlatform(platform, package, + if (!pluginSupportsPlatform(platform, package, requiredMode: PlatformSupport.inline)) { - testPlatforms.add(platform); - } else { print('No implementation for ${_platforms[platform]!.label}.'); + continue; } + if (!pluginHasNativeCodeForPlatform(platform, package)) { + print('No native code for ${_platforms[platform]!.label}.'); + continue; + } + testPlatforms.add(platform); } if (testPlatforms.isEmpty) { - return PackageResult.skip('Not implemented for target platform(s).'); + return PackageResult.skip('Nothing to test for target platform(s).'); } final _TestMode mode = _TestMode( @@ -228,6 +239,8 @@ this command. final bool hasIntegrationTests = exampleHasNativeIntegrationTests(example); + // TODO(stuartmorgan): Make !hasUnitTests fatal. See + // https://github.com/flutter/flutter/issues/85469 if (mode.unit && !hasUnitTests) { _printNoExampleTestsMessage(example, 'Android unit'); } @@ -335,6 +348,9 @@ this command. for (final RepositoryPackage example in plugin.getExamples()) { final String exampleName = example.displayName; + // TODO(stuartmorgan): Always check for RunnerTests, and make it fatal if + // no examples have it. See + // https://github.com/flutter/flutter/issues/85469 if (testTarget != null) { final Directory project = example.directory .childDirectory(platform.toLowerCase()) @@ -387,6 +403,71 @@ this command. return _PlatformResult(overallResult); } + Future<_PlatformResult> _testWindows( + RepositoryPackage plugin, _TestMode mode) async { + if (mode.integrationOnly) { + return _PlatformResult(RunState.skipped); + } + + bool isTestBinary(File file) { + return file.basename.endsWith('_test.exe') || + file.basename.endsWith('_tests.exe'); + } + + return _runGoogleTestTests(plugin, + buildDirectoryName: 'windows', isTestBinary: isTestBinary); + } + + /// Finds every file in the [buildDirectoryName] subdirectory of [plugin]'s + /// build directory for which [isTestBinary] is true, and runs all of them, + /// returning the overall result. + /// + /// The binaries are assumed to be Google Test test binaries, thus returning + /// zero for success and non-zero for failure. + Future<_PlatformResult> _runGoogleTestTests( + RepositoryPackage plugin, { + required String buildDirectoryName, + required bool Function(File) isTestBinary, + }) async { + final List testBinaries = []; + for (final RepositoryPackage example in plugin.getExamples()) { + final Directory buildDir = example.directory + .childDirectory('build') + .childDirectory(buildDirectoryName); + if (!buildDir.existsSync()) { + continue; + } + testBinaries.addAll(buildDir + .listSync(recursive: true) + .whereType() + .where(isTestBinary) + .where((File file) { + // Only run the debug build of the unit tests, to avoid running the + // same tests multiple times. + final List components = path.split(file.path); + return components.contains('debug') || components.contains('Debug'); + })); + } + + if (testBinaries.isEmpty) { + final String binaryExtension = platform.isWindows ? '.exe' : ''; + printError( + 'No test binaries found. At least one *_test(s)$binaryExtension ' + 'binary should be built by the example(s)'); + return _PlatformResult(RunState.failed, + error: 'No $buildDirectoryName unit tests found'); + } + + bool passing = true; + for (final File test in testBinaries) { + print('Running ${test.basename}...'); + final int exitCode = + await processRunner.runAndStream(test.path, []); + passing &= exitCode == 0; + } + return _PlatformResult(passing ? RunState.succeeded : RunState.failed); + } + /// Prints a standard format message indicating that [platform] tests for /// [plugin]'s [example] are about to be run. void _printRunningExampleTestsMessage( diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index aafe7868d8d0..e210152ecf09 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -18,6 +18,7 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; import 'common/core.dart'; +import 'common/file_utils.dart'; import 'common/git_version_finder.dart'; import 'common/plugin_command.dart'; import 'common/process_runner.dart'; @@ -154,13 +155,10 @@ class PublishPluginCommand extends PluginCommand { await gitVersionFinder.getChangedPubSpecs(); for (final String pubspecPath in changedPubspecs) { - // Convert git's Posix-style paths to a path that matches the current - // filesystem. - final String localStylePubspecPath = - path.joinAll(p.posix.split(pubspecPath)); - final File pubspecFile = packagesDir.fileSystem - .directory((await gitDir).path) - .childFile(localStylePubspecPath); + // git outputs a relativa, Posix-style path. + final File pubspecFile = childFileWithSubcomponents( + packagesDir.fileSystem.directory((await gitDir).path), + p.posix.split(pubspecPath)); yield PackageEnumerationEntry(RepositoryPackage(pubspecFile.parent), excluded: false); } diff --git a/script/tool/test/common/file_utils_test.dart b/script/tool/test/common/file_utils_test.dart new file mode 100644 index 000000000000..e3986842a969 --- /dev/null +++ b/script/tool/test/common/file_utils_test.dart @@ -0,0 +1,32 @@ +// 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:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/file_utils.dart'; +import 'package:test/test.dart'; + +void main() { + test('works on Posix', () async { + final FileSystem fileSystem = + MemoryFileSystem(style: FileSystemStyle.posix); + + final Directory base = fileSystem.directory('/').childDirectory('base'); + final File file = + childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); + + expect(file.absolute.path, '/base/foo/bar/baz.txt'); + }); + + test('works on Windows', () async { + final FileSystem fileSystem = + MemoryFileSystem(style: FileSystemStyle.windows); + + final Directory base = fileSystem.directory(r'C:\').childDirectory('base'); + final File file = + childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); + + expect(file.absolute.path, r'C:\base\foo\bar\baz.txt'); + }); +} diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart index 2e08f725eb4b..ac619e2622e0 100644 --- a/script/tool/test/common/plugin_utils_test.dart +++ b/script/tool/test/common/plugin_utils_test.dart @@ -273,4 +273,72 @@ void main() { isTrue); }); }); + + group('pluginHasNativeCodeForPlatform', () { + test('returns false for web', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + }, + )); + + expect(pluginHasNativeCodeForPlatform(kPlatformWeb, plugin), isFalse); + }); + + test('returns false for a native-only plugin', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }, + )); + + expect(pluginHasNativeCodeForPlatform(kPlatformLinux, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(kPlatformMacos, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(kPlatformWindows, plugin), isTrue); + }); + + test('returns true for a native+Dart plugin', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: true, hasDartCode: true), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: true, hasDartCode: true), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: true, hasDartCode: true), + }, + )); + + expect(pluginHasNativeCodeForPlatform(kPlatformLinux, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(kPlatformMacos, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(kPlatformWindows, plugin), isTrue); + }); + + test('returns false for a Dart-only plugin', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: false, hasDartCode: true), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: false, hasDartCode: true), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: false, hasDartCode: true), + }, + )); + + expect(pluginHasNativeCodeForPlatform(kPlatformLinux, plugin), isFalse); + expect(pluginHasNativeCodeForPlatform(kPlatformMacos, plugin), isFalse); + expect(pluginHasNativeCodeForPlatform(kPlatformWindows, plugin), isFalse); + }); + }); } diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index 7b2a3d3ba39c..3613a808d9b8 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -9,6 +9,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/file_utils.dart'; import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:flutter_plugin_tools/src/native_test_command.dart'; import 'package:test/test.dart'; @@ -57,7 +58,7 @@ final Map _kDeviceListMap = { void main() { const String _kDestination = '--ios-destination'; - group('test native_test_command', () { + group('test native_test_command on Posix', () { late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; @@ -164,7 +165,7 @@ void main() { output, containsAllInOrder([ contains('No implementation for iOS.'), - contains('SKIPPING: Not implemented for target platform(s).'), + contains('SKIPPING: Nothing to test for target platform(s).'), ])); expect(processRunner.recordedCalls, orderedEquals([])); }); @@ -181,7 +182,7 @@ void main() { output, containsAllInOrder([ contains('No implementation for iOS.'), - contains('SKIPPING: Not implemented for target platform(s).'), + contains('SKIPPING: Nothing to test for target platform(s).'), ])); expect(processRunner.recordedCalls, orderedEquals([])); }); @@ -291,7 +292,7 @@ void main() { output, containsAllInOrder([ contains('No implementation for macOS.'), - contains('SKIPPING: Not implemented for target platform(s).'), + contains('SKIPPING: Nothing to test for target platform(s).'), ])); expect(processRunner.recordedCalls, orderedEquals([])); }); @@ -309,7 +310,7 @@ void main() { output, containsAllInOrder([ contains('No implementation for macOS.'), - contains('SKIPPING: Not implemented for target platform(s).'), + contains('SKIPPING: Nothing to test for target platform(s).'), ])); expect(processRunner.recordedCalls, orderedEquals([])); }); @@ -707,7 +708,7 @@ void main() { output, containsAllInOrder([ contains('No implementation for Android.'), - contains('SKIPPING: Not implemented for target platform(s).'), + contains('SKIPPING: Nothing to test for target platform(s).'), ]), ); }); @@ -1173,6 +1174,7 @@ void main() { '--android', '--ios', '--macos', + '--windows', _kDestination, 'foo_destination', ]); @@ -1183,7 +1185,38 @@ void main() { contains('No implementation for Android.'), contains('No implementation for iOS.'), contains('No implementation for macOS.'), - contains('SKIPPING: Not implemented for target platform(s).'), + contains('SKIPPING: Nothing to test for target platform(s).'), + ])); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skips Dart-only plugins', () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline, + hasDartCode: true, hasNativeCode: false), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + hasDartCode: true, hasNativeCode: false), + }, + ); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--windows', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('No native code for macOS.'), + contains('No native code for Windows.'), + contains('SKIPPING: Nothing to test for target platform(s).'), ])); expect(processRunner.recordedCalls, orderedEquals([])); @@ -1295,4 +1328,165 @@ void main() { }); }); }); + + group('test native_test_command on Windows', () { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(style: FileSystemStyle.windows); + mockPlatform = MockPlatform(isWindows: true); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final NativeTestCommand command = NativeTestCommand(packagesDir, + processRunner: processRunner, platform: mockPlatform); + + runner = CommandRunner( + 'native_test_command', 'Test for native_test_command'); + runner.addCommand(command); + }); + + group('Windows', () { + test('runs unit tests', () async { + const String testBinaryRelativePath = + 'build/windows/foo/Debug/bar/plugin_test.exe'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$testBinaryRelativePath' + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }); + + final File testBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...testBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test.exe...'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(testBinary.path, const [], null), + ])); + }); + + test('only runs debug unit tests', () async { + const String debugTestBinaryRelativePath = + 'build/windows/foo/Debug/bar/plugin_test.exe'; + const String releaseTestBinaryRelativePath = + 'build/windows/foo/Release/bar/plugin_test.exe'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$debugTestBinaryRelativePath', + 'example/$releaseTestBinaryRelativePath' + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }); + + final File debugTestBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...debugTestBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test.exe...'), + contains('No issues found!'), + ]), + ); + + // Only the debug version should be run. + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(debugTestBinary.path, const [], null), + ])); + }); + + test('fails if there are no unit tests', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No test binaries found.'), + ]), + ); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('fails if a unit test fails', () async { + const String testBinaryRelativePath = + 'build/windows/foo/Debug/bar/plugin_test.exe'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$testBinaryRelativePath' + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }); + + final File testBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...testBinaryRelativePath.split('/')]); + + processRunner.mockProcessesForExecutable[testBinary.path] = + [MockProcess(exitCode: 1)]; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running plugin_test.exe...'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(testBinary.path, const [], null), + ])); + }); + }); + }); } diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 74c036489233..e053100172c6 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -10,6 +10,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/file_utils.dart'; import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:flutter_plugin_tools/src/common/process_runner.dart'; import 'package:meta/meta.dart'; @@ -47,6 +48,8 @@ class PlatformDetails { const PlatformDetails( this.type, { this.variants = const [], + this.hasNativeCode = true, + this.hasDartCode = false, }); /// The type of support for the platform. @@ -54,6 +57,16 @@ class PlatformDetails { /// Any 'supportVariants' to list in the pubspec. final List variants; + + /// Whether or not the plugin includes native code. + /// + /// Ignored for web, which does not have native code. + final bool hasNativeCode; + + /// Whether or not the plugin includes Dart code. + /// + /// Ignored for web, which always has native code. + final bool hasDartCode; } /// Creates a plugin package with the given [name] in [packagesDirectory]. @@ -130,15 +143,10 @@ Directory createFakePackage( } } - final FileSystem fileSystem = packageDirectory.fileSystem; final p.Context posixContext = p.posix; for (final String file in extraFiles) { - final List newFilePath = [ - packageDirectory.path, - ...posixContext.split(file) - ]; - final File newFile = fileSystem.file(fileSystem.path.joinAll(newFilePath)); - newFile.createSync(recursive: true); + childFileWithSubcomponents(packageDirectory, posixContext.split(file)) + .createSync(recursive: true); } return packageDirectory; @@ -210,49 +218,38 @@ String _pluginPlatformSection( default_package: ${packageName}_$platform '''; } else { + final List lines = [ + ' $platform:', + ]; switch (platform) { case kPlatformAndroid: - entry = ''' - android: - package: io.flutter.plugins.fake - pluginClass: FakePlugin -'''; - break; + lines.add(' package: io.flutter.plugins.fake'); + continue nativeByDefault; + nativeByDefault: case kPlatformIos: - entry = ''' - ios: - pluginClass: FLTFakePlugin -'''; - break; case kPlatformLinux: - entry = ''' - linux: - pluginClass: FakePlugin -'''; - break; case kPlatformMacos: - entry = ''' - macos: - pluginClass: FakePlugin -'''; + case kPlatformWindows: + if (support.hasNativeCode) { + final String className = + platform == kPlatformIos ? 'FLTFakePlugin' : 'FakePlugin'; + lines.add(' pluginClass: $className'); + } + if (support.hasDartCode) { + lines.add(' dartPluginClass: FakeDartPlugin'); + } break; case kPlatformWeb: - entry = ''' - web: - pluginClass: FakePlugin - fileName: ${packageName}_web.dart -'''; - break; - case kPlatformWindows: - entry = ''' - windows: - pluginClass: FakePlugin -'''; + lines.addAll([ + ' pluginClass: FakePlugin', + ' fileName: ${packageName}_web.dart', + ]); break; default: assert(false, 'Unrecognized platform: $platform'); break; } + entry = lines.join('\n') + '\n'; } // Add any variants. From ffe53ec2f135ae01fa1a691c01d5672fe54a0392 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 31 Aug 2021 10:54:41 -0400 Subject: [PATCH 117/123] [flutter_plugin_tool] Move branch-switching logic from tool_runner.sh to tool (#4268) Eliminates the remaining logic from tool_runner.sh, completing the goal of migrating repository tooling off of bash (both to make maintenance easier, and to better support Windows both locally and in CI). Its branch-based logic is now part of the tool itself, via a new `--packages-for-branch` flag (which is hidden in help since it's only useful for CI). Part of https://github.com/flutter/flutter/issues/86113 --- script/tool/CHANGELOG.md | 4 +- .../tool/lib/src/common/plugin_command.dart | 90 +++++-- script/tool/pubspec.yaml | 2 +- .../tool/test/common/plugin_command_test.dart | 247 +++++++++++++----- script/tool_runner.sh | 31 +-- 5 files changed, 271 insertions(+), 103 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 0edb106f099c..2ef34c184b11 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,4 +1,4 @@ -## NEXT +## 0.6.0 - Added Android native integration test support to `native-test`. - Added a new `android-lint` command to lint Android plugin native code. @@ -10,6 +10,8 @@ `--no-push-flags`. Releases now always tag and push. - **Breaking change**: `publish`'s `--package` flag has been replaced with the `--packages` flag used by most other packages. +- **Breaking change** Passing both `--run-on-changed-packages` and `--packages` + is now an error; previously it the former would be ignored. ## 0.5.0 diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index ec51261ab617..514a90b85cc7 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io' as io; import 'dart:math'; import 'package:args/command_runner.dart'; @@ -72,11 +73,18 @@ abstract class PluginCommand extends Command { ); argParser.addFlag(_runOnChangedPackagesArg, help: 'Run the command on changed packages/plugins.\n' - 'If the $_packagesArg is specified, this flag is ignored.\n' 'If no packages have changed, or if there have been changes that may\n' 'affect all packages, the command runs on all packages.\n' 'The packages excluded with $_excludeArg is also excluded even if changed.\n' - 'See $_kBaseSha if a custom base is needed to determine the diff.'); + 'See $_kBaseSha if a custom base is needed to determine the diff.\n\n' + 'Cannot be combined with $_packagesArg.\n'); + argParser.addFlag(_packagesForBranchArg, + help: + 'This runs on all packages (equivalent to no package selection flag)\n' + 'on master, and behaves like --run-on-changed-packages on any other branch.\n\n' + 'Cannot be combined with $_packagesArg.\n\n' + 'This is intended for use in CI.\n', + hide: true); argParser.addOption(_kBaseSha, help: 'The base sha used to determine git diff. \n' 'This is useful when $_runOnChangedPackagesArg is specified.\n' @@ -89,6 +97,7 @@ abstract class PluginCommand extends Command { static const String _shardCountArg = 'shardCount'; static const String _excludeArg = 'exclude'; static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; + static const String _packagesForBranchArg = 'packages-for-branch'; static const String _kBaseSha = 'base-sha'; /// The directory containing the plugin packages. @@ -266,15 +275,50 @@ abstract class PluginCommand extends Command { /// is a sibling of the packages directory. This is used for a small number /// of packages in the flutter/packages repository. Stream _getAllPackages() async* { + final Set packageSelectionFlags = { + _packagesArg, + _runOnChangedPackagesArg, + _packagesForBranchArg, + }; + if (packageSelectionFlags + .where((String flag) => argResults!.wasParsed(flag)) + .length > + 1) { + printError('Only one of --$_packagesArg, --$_runOnChangedPackagesArg, or ' + '--$_packagesForBranchArg can be provided.'); + throw ToolExit(exitInvalidArguments); + } + Set plugins = Set.from(getStringListArg(_packagesArg)); + final bool runOnChangedPackages; + if (getBoolArg(_runOnChangedPackagesArg)) { + runOnChangedPackages = true; + } else if (getBoolArg(_packagesForBranchArg)) { + final String? branch = await _getBranch(); + if (branch == null) { + printError('Unabled to determine branch; --$_packagesForBranchArg can ' + 'only be used in a git repository.'); + throw ToolExit(exitInvalidArguments); + } else { + runOnChangedPackages = branch != 'master'; + // Log the mode for auditing what was intended to run. + print('--$_packagesForBranchArg: running on ' + '${runOnChangedPackages ? 'changed' : 'all'} packages'); + } + } else { + runOnChangedPackages = false; + } + final Set excludedPluginNames = getExcludedPackageNames(); - final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); - if (plugins.isEmpty && - runOnChangedPackages && - !(await _changesRequireFullTest())) { - plugins = await _getChangedPackages(); + if (runOnChangedPackages) { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final List changedFiles = + await gitVersionFinder.getChangedFiles(); + if (!_changesRequireFullTest(changedFiles)) { + plugins = _getChangedPackages(changedFiles); + } } final Directory thirdPartyPackagesDirectory = packagesDir.parent @@ -374,15 +418,13 @@ abstract class PluginCommand extends Command { return gitVersionFinder; } - // Returns packages that have been changed relative to the git base. - Future> _getChangedPackages() async { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - - final List allChangedFiles = - await gitVersionFinder.getChangedFiles(); + // Returns packages that have been changed given a list of changed files. + // + // The paths must use POSIX separators (e.g., as provided by git output). + Set _getChangedPackages(List changedFiles) { final Set packages = {}; - for (final String path in allChangedFiles) { - final List pathComponents = path.split('/'); + for (final String path in changedFiles) { + final List pathComponents = p.posix.split(path); final int packagesIndex = pathComponents.indexWhere((String element) => element == 'packages'); if (packagesIndex != -1) { @@ -398,11 +440,19 @@ abstract class PluginCommand extends Command { return packages; } + Future _getBranch() async { + final io.ProcessResult branchResult = await (await gitDir).runCommand( + ['rev-parse', '--abbrev-ref', 'HEAD'], + throwOnError: false); + if (branchResult.exitCode != 0) { + return null; + } + return (branchResult.stdout as String).trim(); + } + // Returns true if one or more files changed that have the potential to affect // any plugin (e.g., CI script changes). - Future _changesRequireFullTest() async { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - + bool _changesRequireFullTest(List changedFiles) { const List specialFiles = [ '.ci.yaml', // LUCI config. '.cirrus.yml', // Cirrus config. @@ -417,9 +467,7 @@ abstract class PluginCommand extends Command { // check below is done via string prefixing. assert(specialDirectories.every((String dir) => dir.endsWith('/'))); - final List allChangedFiles = - await gitVersionFinder.getChangedFiles(); - return allChangedFiles.any((String path) => + return changedFiles.any((String path) => specialFiles.contains(path) || specialDirectories.any((String dir) => path.startsWith(dir))); } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 02b3ca624b96..7c2bb0b3e3c0 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.5.0 +version: 0.6.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 10bdff4e9c56..3ef0d3b3c005 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/plugin_command.dart'; import 'package:flutter_plugin_tools/src/common/process_runner.dart'; import 'package:git/git.dart'; @@ -28,8 +29,6 @@ void main() { late MockPlatform mockPlatform; late Directory packagesDir; late Directory thirdPartyPackagesDir; - late List?> gitDirCommands; - late String gitDiffResponse; setUp(() { fileSystem = MemoryFileSystem(); @@ -39,18 +38,15 @@ void main() { .childDirectory('third_party') .childDirectory('packages'); - gitDirCommands = ?>[]; - gitDiffResponse = ''; final MockGitDir gitDir = MockGitDir(); when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) .thenAnswer((Invocation invocation) { - gitDirCommands.add(invocation.positionalArguments[0] as List?); - final MockProcessResult mockProcessResult = MockProcessResult(); - if (invocation.positionalArguments[0][0] == 'diff') { - when(mockProcessResult.stdout as String?) - .thenReturn(gitDiffResponse); - } - return Future.value(mockProcessResult); + final List arguments = + invocation.positionalArguments[0]! as List; + // Attach the first argument to the command to make targeting the mock + // results easier. + final String gitCommand = arguments.removeAt(0); + return processRunner.run('git-$gitCommand', arguments); }); processRunner = RecordingProcessRunner(); command = SamplePluginCommand( @@ -184,6 +180,68 @@ void main() { expect(command.plugins, unorderedEquals([])); }); + group('conflicting package selection', () { + test('does not allow --packages with --run-on-changed-packages', + () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'sample', + '--run-on-changed-packages', + '--packages=plugin1', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Only one of --packages, --run-on-changed-packages, or ' + '--packages-for-branch can be provided.') + ])); + }); + + test('does not allow --packages with --packages-for-branch', () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'sample', + '--packages-for-branch', + '--packages=plugin1', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Only one of --packages, --run-on-changed-packages, or ' + '--packages-for-branch can be provided.') + ])); + }); + + test( + 'does not allow --run-on-changed-packages with --packages-for-branch', + () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'sample', + '--packages-for-branch', + '--packages=plugin1', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Only one of --packages, --run-on-changed-packages, or ' + '--packages-for-branch can be provided.') + ])); + }); + }); + group('test run-on-changed-packages', () { test('all plugins should be tested if there are no changes.', () async { final Directory plugin1 = createFakePlugin('plugin1', packagesDir); @@ -201,7 +259,9 @@ void main() { test( 'all plugins should be tested if there are no plugin related changes.', () async { - gitDiffResponse = 'AUTHORS'; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'AUTHORS'), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -215,10 +275,12 @@ void main() { }); test('all plugins should be tested if .cirrus.yml changes.', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' .cirrus.yml packages/plugin1/CHANGELOG -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -232,10 +294,12 @@ packages/plugin1/CHANGELOG }); test('all plugins should be tested if .ci.yaml changes', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' .ci.yaml packages/plugin1/CHANGELOG -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -250,10 +314,12 @@ packages/plugin1/CHANGELOG test('all plugins should be tested if anything in .ci/ changes', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' .ci/Dockerfile packages/plugin1/CHANGELOG -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -268,10 +334,12 @@ packages/plugin1/CHANGELOG test('all plugins should be tested if anything in script changes.', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' script/tool_runner.sh packages/plugin1/CHANGELOG -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -286,10 +354,12 @@ packages/plugin1/CHANGELOG test('all plugins should be tested if the root analysis options change.', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' analysis_options.yaml packages/plugin1/CHANGELOG -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -304,10 +374,12 @@ packages/plugin1/CHANGELOG test('all plugins should be tested if formatting options change.', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' .clang-format packages/plugin1/CHANGELOG -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -321,7 +393,9 @@ packages/plugin1/CHANGELOG }); test('Only changed plugin should be tested.', () async { - gitDiffResponse = 'packages/plugin1/plugin1.dart'; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -335,10 +409,12 @@ packages/plugin1/CHANGELOG test('multiple files in one plugin should also test the plugin', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' packages/plugin1/plugin1.dart packages/plugin1/ios/plugin1.m -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ @@ -352,10 +428,12 @@ packages/plugin1/ios/plugin1.m test('multiple plugins changed should test all the changed plugins', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); @@ -372,11 +450,13 @@ packages/plugin2/ios/plugin2.m test( 'multiple plugins inside the same plugin group changed should output the plugin group name', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' packages/plugin1/plugin1/plugin1.dart packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart packages/plugin1/plugin1_web/plugin1_web.dart -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); @@ -393,9 +473,11 @@ packages/plugin1/plugin1_web/plugin1_web.dart test( 'changing one plugin in a federated group should include all plugins in the group', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' packages/plugin1/plugin1/plugin1.dart -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); final Directory plugin2 = createFakePlugin('plugin1_platform_interface', @@ -414,35 +496,14 @@ packages/plugin1/plugin1/plugin1.dart [plugin1.path, plugin2.path, plugin3.path])); }); - test( - '--packages flag overrides the behavior of --run-on-changed-packages', - () async { - gitDiffResponse = ''' -packages/plugin1/plugin1.dart -packages/plugin2/ios/plugin2.m -packages/plugin3/plugin3.dart -'''; - final Directory plugin1 = - createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - createFakePlugin('plugin3', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--packages=plugin1,plugin2', - '--base-sha=master', - '--run-on-changed-packages' - ]); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - }); - test('--exclude flag works with --run-on-changed-packages', () async { - gitDiffResponse = ''' + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m packages/plugin3/plugin3.dart -'''; +'''), + ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); @@ -459,6 +520,74 @@ packages/plugin3/plugin3.dart }); }); + group('--packages-for-branch', () { + test('only tests changed packages on a branch', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; + processRunner.mockProcessesForExecutable['git-rev-parse'] = [ + MockProcess(stdout: 'a-branch'), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + + final List output = await runCapturingPrint( + runner, ['sample', '--packages-for-branch']); + + expect(command.plugins, unorderedEquals([plugin1.path])); + expect( + output, + containsAllInOrder([ + contains('--packages-for-branch: running on changed packages'), + ])); + }); + + test('tests all packages on master', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; + processRunner.mockProcessesForExecutable['git-rev-parse'] = [ + MockProcess(stdout: 'master'), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + + final List output = await runCapturingPrint( + runner, ['sample', '--packages-for-branch']); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + expect( + output, + containsAllInOrder([ + contains('--packages-for-branch: running on all packages'), + ])); + }); + + test('throws if getting the branch fails', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; + processRunner.mockProcessesForExecutable['git-rev-parse'] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['sample', '--packages-for-branch'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unabled to determine branch'), + ])); + }); + }); + group('sharding', () { test('distributes evenly when evenly divisible', () async { final List> expectedShards = >[ @@ -625,5 +754,3 @@ class SamplePluginCommand extends PluginCommand { } } } - -class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool_runner.sh b/script/tool_runner.sh index 93a7776d0a35..99bab387e6b6 100755 --- a/script/tool_runner.sh +++ b/script/tool_runner.sh @@ -5,27 +5,18 @@ set -e +# WARNING! Do not remove this script, or change its behavior, unless you have +# verified that it will not break the flutter/flutter analysis run of this +# repository: https://github.com/flutter/flutter/blob/master/dev/bots/test.dart + readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" +readonly TOOL_PATH="$REPO_DIR/script/tool/bin/flutter_plugin_tools.dart" -# Runs the plugin tools from the in-tree source. -function plugin_tools() { - (pushd "$REPO_DIR/script/tool" && dart pub get && popd) >/dev/null - dart run "$REPO_DIR/script/tool/bin/flutter_plugin_tools.dart" "$@" -} - -ACTIONS=("$@") - -BRANCH_NAME="${BRANCH_NAME:-"$(git rev-parse --abbrev-ref HEAD)"}" - -# This has to be turned into a list and then split out to the command line, -# otherwise it gets treated as a single argument. -PLUGIN_SHARDING=($PLUGIN_SHARDING) +# Ensure that the tool dependencies have been fetched. +(pushd "$REPO_DIR/script/tool" && dart pub get && popd) >/dev/null -if [[ "${BRANCH_NAME}" == "master" ]]; then - echo "Running for all packages" - (cd "$REPO_DIR" && plugin_tools "${ACTIONS[@]}" ${PLUGIN_SHARDING[@]}) -else - echo running "${ACTIONS[@]}" - (cd "$REPO_DIR" && plugin_tools "${ACTIONS[@]}" --run-on-changed-packages ${PLUGIN_SHARDING[@]}) -fi +# The tool expects to be run from the repo root. +cd "$REPO_DIR" +# Run from the in-tree source. +dart run "$TOOL_PATH" "$@" --packages-for-branch $PLUGIN_SHARDING From 5afbfe9bb7b57df299b1bcac7dbf4517823388cc Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 31 Aug 2021 13:55:01 -0400 Subject: [PATCH 118/123] [flutter_plugin_tool] Migrate 'publish' to new base command (#4290) Moves `publish` to PackageLoopingCommand, giving it the same standardized output and summary that is used by most other commands in the tool. Adds minor new functionality to the base command to allow it to handle the specific needs of publish: - Allows fully customizing the set of packages to loop over, to support --all-changed - Allows customization of a few more strings so the output better matches the functionality (e.g., using 'published' instead of 'ran' in the summary lines). Fixes https://github.com/flutter/flutter/issues/83413 --- .../src/common/package_looping_command.dart | 22 +- .../tool/lib/src/publish_plugin_command.dart | 239 ++++++------------ .../test/publish_plugin_command_test.dart | 140 +++++----- 3 files changed, 163 insertions(+), 238 deletions(-) diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 00caeb30ef42..96dd881bfe00 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -92,6 +92,18 @@ abstract class PackageLoopingCommand extends PluginCommand { /// arguments are invalid), and to set up any run-level state. Future initializeRun() async {} + /// Returns the packages to process. By default, this returns the packages + /// defined by the standard tooling flags and the [inculdeSubpackages] option, + /// but can be overridden for custom package enumeration. + /// + /// Note: Consistent behavior across commands whenever possibel is a goal for + /// this tool, so this should be overridden only in rare cases. + Stream getPackagesToProcess() async* { + yield* includeSubpackages + ? getTargetPackagesAndSubpackages(filterExcluded: false) + : getTargetPackages(filterExcluded: false); + } + /// Runs the command for [package], returning a list of errors. /// /// Errors may either be an empty string if there is no context that should @@ -138,6 +150,9 @@ abstract class PackageLoopingCommand extends PluginCommand { /// context. String get failureListFooter => 'See above for full details.'; + /// The summary string used for a successful run in the final overview output. + String get successSummaryMessage => 'ran'; + /// If true, all printing (including the summary) will be redirected to a /// buffer, and provided in a call to [handleCapturedOutput] at the end of /// the run. @@ -206,9 +221,8 @@ abstract class PackageLoopingCommand extends PluginCommand { await initializeRun(); - final List targetPackages = includeSubpackages - ? await getTargetPackagesAndSubpackages(filterExcluded: false).toList() - : await getTargetPackages(filterExcluded: false).toList(); + final List targetPackages = + await getPackagesToProcess().toList(); final Map results = {}; @@ -346,7 +360,7 @@ abstract class PackageLoopingCommand extends PluginCommand { summary = 'skipped'; style = hadWarning ? Styles.LIGHT_YELLOW : Styles.DARK_GRAY; } else { - summary = 'ran'; + summary = successSummaryMessage; style = hadWarning ? Styles.YELLOW : Styles.GREEN; } if (hadWarning) { diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index e210152ecf09..6da51706ef1e 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -7,7 +7,6 @@ import 'dart:convert'; import 'dart:io' as io; import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:git/git.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; @@ -20,9 +19,11 @@ import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/file_utils.dart'; import 'common/git_version_finder.dart'; +import 'common/package_looping_command.dart'; import 'common/plugin_command.dart'; import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; +import 'common/repository_package.dart'; @immutable class _RemoteInfo { @@ -46,7 +47,7 @@ class _RemoteInfo { /// usage information. /// /// [processRunner], [print], and [stdin] can be overriden for easier testing. -class PublishPluginCommand extends PluginCommand { +class PublishPluginCommand extends PackageLoopingCommand { /// Creates an instance of the publish command. PublishPluginCommand( Directory packagesDir, { @@ -117,38 +118,45 @@ class PublishPluginCommand extends PluginCommand { StreamSubscription? _stdinSubscription; final PubVersionFinder _pubVersionFinder; + // Tags that already exist in the repository. + List _existingGitTags = []; + // The remote to push tags to. + late _RemoteInfo _remote; + + @override + String get successSummaryMessage => 'published'; + + @override + String get failureListHeader => + 'The following packages had failures during publishing:'; + @override - Future run() async { + Future initializeRun() async { print('Checking local repo...'); - final GitDir repository = await gitDir; + + // Ensure that the requested remote is present. final String remoteName = getStringArg(_remoteOption); final String? remoteUrl = await _verifyRemote(remoteName); if (remoteUrl == null) { printError('Unable to find URL for remote $remoteName; cannot push tags'); throw ToolExit(1); } - final _RemoteInfo remote = _RemoteInfo(name: remoteName, url: remoteUrl); + _remote = _RemoteInfo(name: remoteName, url: remoteUrl); + + // Pre-fetch all the repository's tags, to check against when publishing. + final GitDir repository = await gitDir; + final io.ProcessResult existingTagsResult = + await repository.runCommand(['tag', '--sort=-committerdate']); + _existingGitTags = (existingTagsResult.stdout as String).split('\n') + ..removeWhere((String element) => element.isEmpty); - print('Local repo is ready!'); if (getBoolArg(_dryRunFlag)) { - print('=============== DRY RUN ==============='); + print('=============== DRY RUN ==============='); } - - final List packages = await _getPackagesToProcess() - .where((PackageEnumerationEntry entry) => !entry.excluded) - .toList(); - bool successful = true; - - successful = await _publishPackages( - packages, - baseGitDir: repository, - remoteForTagPush: remote, - ); - - await _finish(successful); } - Stream _getPackagesToProcess() async* { + @override + Stream getPackagesToProcess() async* { if (getBoolArg(_allChangedFlag)) { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); final List changedPubspecs = @@ -167,92 +175,52 @@ class PublishPluginCommand extends PluginCommand { } } - Future _publishPackages( - List packages, { - required GitDir baseGitDir, - required _RemoteInfo remoteForTagPush, - }) async { - if (packages.isEmpty) { - print('No version updates in this commit.'); - return true; + @override + Future runForPackage(RepositoryPackage package) async { + final PackageResult? checkResult = await _checkNeedsRelease(package); + if (checkResult != null) { + return checkResult; } - final io.ProcessResult existingTagsResult = - await baseGitDir.runCommand(['tag', '--sort=-committerdate']); - final List existingTags = (existingTagsResult.stdout as String) - .split('\n') - ..removeWhere((String element) => element.isEmpty); - - final List packagesReleased = []; - final List packagesFailed = []; - - for (final PackageEnumerationEntry entry in packages) { - final RepositoryPackage package = entry.package; - - final _CheckNeedsReleaseResult result = await _checkNeedsRelease( - package: package, - existingTags: existingTags, - ); - switch (result) { - case _CheckNeedsReleaseResult.release: - break; - case _CheckNeedsReleaseResult.noRelease: - continue; - case _CheckNeedsReleaseResult.failure: - packagesFailed.add(package.displayName); - continue; - } - print('\n'); - if (await _publishAndTagPackage(package, - remoteForTagPush: remoteForTagPush)) { - packagesReleased.add(package.displayName); - } else { - packagesFailed.add(package.displayName); - } - print('\n'); + if (!await _checkGitStatus(package)) { + return PackageResult.fail(['uncommitted changes']); } - if (packagesReleased.isNotEmpty) { - print('Packages released: ${packagesReleased.join(', ')}'); + + if (!await _publish(package)) { + return PackageResult.fail(['publish failed']); } - if (packagesFailed.isNotEmpty) { - printError( - 'Failed to release the following packages: ${packagesFailed.join(', ')}, see above for details.'); + + if (!await _tagRelease(package)) { + return PackageResult.fail(['tagging failed']); } - return packagesFailed.isEmpty; + + print('\nPublished ${package.directory.basename} successfully!'); + return PackageResult.success(); } - // Publish the package to pub with `pub publish`, then git tag the release - // and push the tag to [remoteForTagPush]. - // Returns `true` if publishing and tagging are successful. - Future _publishAndTagPackage( - RepositoryPackage package, { - _RemoteInfo? remoteForTagPush, - }) async { - if (!await _publishPackage(package)) { - return false; - } - if (!await _tagRelease( - package, - remoteForPush: remoteForTagPush, - )) { - return false; - } - print('Published ${package.directory.basename} successfully.'); - return true; + @override + Future completeRun() async { + _pubVersionFinder.httpClient.close(); + await _stdinSubscription?.cancel(); + _stdinSubscription = null; } - // Returns a [_CheckNeedsReleaseResult] that indicates the result. - Future<_CheckNeedsReleaseResult> _checkNeedsRelease({ - required RepositoryPackage package, - required List existingTags, - }) async { + /// Checks whether [package] needs to be released, printing check status and + /// returning one of: + /// - PackageResult.fail if the check could not be completed + /// - PackageResult.skip if no release is necessary + /// - null if releasing should proceed + /// + /// In cases where a non-null result is returned, that should be returned + /// as the final result for the package, without further processing. + Future _checkNeedsRelease(RepositoryPackage package) async { final File pubspecFile = package.pubspecFile; if (!pubspecFile.existsSync()) { - print(''' + logWarning(''' The pubspec file at ${pubspecFile.path} does not exist. Publishing will not happen for ${pubspecFile.parent.basename}. Safe to ignore if the package is deleted in this commit. '''); - return _CheckNeedsReleaseResult.noRelease; + return PackageResult.skip('package deleted'); } final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); @@ -261,17 +229,18 @@ Safe to ignore if the package is deleted in this commit. // Ignore flutter_plugin_tools package when running publishing through flutter_plugin_tools. // TODO(cyanglaz): Make the tool also auto publish flutter_plugin_tools package. // https://github.com/flutter/flutter/issues/85430 - return _CheckNeedsReleaseResult.noRelease; + return PackageResult.skip( + 'publishing flutter_plugin_tools via the tool is not supported'); } if (pubspec.publishTo == 'none') { - return _CheckNeedsReleaseResult.noRelease; + return PackageResult.skip('publish_to: none'); } if (pubspec.version == null) { printError( 'No version found. A package that intentionally has no version should be marked "publish_to: none"'); - return _CheckNeedsReleaseResult.failure; + return PackageResult.fail(['no version']); } // Check if the package named `packageName` with `version` has already @@ -280,49 +249,29 @@ Safe to ignore if the package is deleted in this commit. final PubVersionFinderResponse pubVersionFinderResponse = await _pubVersionFinder.getPackageVersion(packageName: pubspec.name); if (pubVersionFinderResponse.versions.contains(version)) { - final String tagsForPackageWithSameVersion = existingTags.firstWhere( + final String tagsForPackageWithSameVersion = _existingGitTags.firstWhere( (String tag) => tag.split('-v').first == pubspec.name && tag.split('-v').last == version.toString(), orElse: () => ''); - print( - 'The version $version of ${pubspec.name} has already been published'); if (tagsForPackageWithSameVersion.isEmpty) { printError( - 'However, the git release tag for this version (${pubspec.name}-v$version) is not found. Please manually fix the tag then run the command again.'); - return _CheckNeedsReleaseResult.failure; + '${pubspec.name} $version has already been published, however ' + 'the git release tag (${pubspec.name}-v$version) was not found. ' + 'Please manually fix the tag then run the command again.'); + return PackageResult.fail(['published but untagged']); } else { - print('skip.'); - return _CheckNeedsReleaseResult.noRelease; + print('${pubspec.name} $version has already been published.'); + return PackageResult.skip('already published'); } } - return _CheckNeedsReleaseResult.release; - } - - // Publish the package. - // - // Returns `true` if successful, `false` otherwise. - Future _publishPackage(RepositoryPackage package) async { - final bool gitStatusOK = await _checkGitStatus(package); - if (!gitStatusOK) { - return false; - } - final bool publishOK = await _publish(package); - if (!publishOK) { - return false; - } - print('Package published!'); - return true; + return null; } - // Tag the release with -v, and, if [remoteForTagPush] - // is provided, push it to that remote. + // Tag the release with -v, and push it to the remote. // // Return `true` if successful, `false` otherwise. - Future _tagRelease( - RepositoryPackage package, { - _RemoteInfo? remoteForPush, - }) async { + Future _tagRelease(RepositoryPackage package) async { final String tag = _getTag(package); print('Tagging release $tag...'); if (!getBoolArg(_dryRunFlag)) { @@ -335,27 +284,15 @@ Safe to ignore if the package is deleted in this commit. } } - if (remoteForPush == null) { - return true; - } - - print('Pushing tag to ${remoteForPush.name}...'); - return await _pushTagToRemote( + print('Pushing tag to ${_remote.name}...'); + final bool success = await _pushTagToRemote( tag: tag, - remote: remoteForPush, + remote: _remote, ); - } - - Future _finish(bool successful) async { - _pubVersionFinder.httpClient.close(); - await _stdinSubscription?.cancel(); - _stdinSubscription = null; - if (successful) { - print('Done!'); - } else { - printError('Failed, see above for details.'); - throw ToolExit(1); + if (success) { + print('Release tagged!'); } + return success; } Future _checkGitStatus(RepositoryPackage package) async { @@ -394,6 +331,7 @@ Safe to ignore if the package is deleted in this commit. } Future _publish(RepositoryPackage package) async { + print('Publishing...'); final List publishFlags = getStringListArg(_pubFlagsOption); print('Running `pub publish ${publishFlags.join(' ')}` in ' '${package.directory.absolute.path}...\n'); @@ -421,6 +359,8 @@ Safe to ignore if the package is deleted in this commit. printError('Publishing ${package.directory.basename} failed.'); return false; } + + print('Package published!'); return true; } @@ -516,14 +456,3 @@ final String _credentialsPath = () { return p.join(cacheDir, 'credentials.json'); }(); - -enum _CheckNeedsReleaseResult { - // The package needs to be released. - release, - - // The package does not need to be released. - noRelease, - - // There's an error when trying to determine whether the package needs to be released. - failure, -} diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index ae3d768fcc70..2ea4fc753460 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -108,7 +108,8 @@ void main() { '?? /packages/foo/tmp\n\n' 'If the directory should be clean, you can run `git clean -xdf && ' 'git reset --hard HEAD` to wipe all local changes.'), - contains('Failed, see above for details.'), + contains('foo:\n' + ' uncommitted changes'), ])); }); @@ -264,10 +265,13 @@ void main() { isNot(contains('git-push'))); expect( output, - containsAllInOrder([ - '=============== DRY RUN ===============', - 'Running `pub publish ` in ${pluginDir.path}...\n', - 'Done!' + containsAllInOrder([ + contains('=============== DRY RUN ==============='), + contains('Running for foo'), + contains('Running `pub publish ` in ${pluginDir.path}...'), + contains('Tagging release foo-v0.0.1...'), + contains('Pushing tag to upstream...'), + contains('Published foo successfully!'), ])); }); @@ -353,7 +357,8 @@ void main() { expect( output, containsAllInOrder([ - contains('Published foo successfully.'), + contains('Pushing tag to upstream...'), + contains('Published foo successfully!'), ])); }); @@ -376,7 +381,7 @@ void main() { expect( output, containsAllInOrder([ - contains('Published foo successfully.'), + contains('Published foo successfully!'), ])); }); @@ -395,12 +400,12 @@ void main() { isNot(contains('git-push'))); expect( output, - containsAllInOrder([ - '=============== DRY RUN ===============', - 'Running `pub publish ` in ${pluginDir.path}...\n', - 'Tagging release foo-v0.0.1...', - 'Pushing tag to upstream...', - 'Done!' + containsAllInOrder([ + contains('=============== DRY RUN ==============='), + contains('Running `pub publish ` in ${pluginDir.path}...'), + contains('Tagging release foo-v0.0.1...'), + contains('Pushing tag to upstream...'), + contains('Published foo successfully!'), ])); }); @@ -424,7 +429,7 @@ void main() { expect( output, containsAllInOrder([ - contains('Published foo successfully.'), + contains('Published foo successfully!'), ])); }); }); @@ -460,13 +465,11 @@ void main() { expect( output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2/plugin2', - 'Done!' + containsAllInOrder([ + contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Running `pub publish ` in ${pluginDir2.path}...'), + contains('plugin1 - \x1B[32mpublished\x1B[0m'), + contains('plugin2/plugin2 - \x1B[32mpublished\x1B[0m'), ])); expect( processRunner.recordedCalls, @@ -522,12 +525,8 @@ void main() { expect( output, containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', 'Running `pub publish ` in ${pluginDir1.path}...\n', 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2/plugin2', - 'Done!' ])); expect( processRunner.recordedCalls, @@ -573,18 +572,16 @@ void main() { expect( output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - '=============== DRY RUN ===============', - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'Tagging release plugin1-v0.0.1...', - 'Pushing tag to upstream...', - 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Tagging release plugin2-v0.0.1...', - 'Pushing tag to upstream...', - 'Packages released: plugin1, plugin2/plugin2', - 'Done!' + containsAllInOrder([ + contains('=============== DRY RUN ==============='), + contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Tagging release plugin1-v0.0.1...'), + contains('Pushing tag to upstream...'), + contains('Published plugin1 successfully!'), + contains('Running `pub publish ` in ${pluginDir2.path}...'), + contains('Tagging release plugin2-v0.0.1...'), + contains('Pushing tag to upstream...'), + contains('Published plugin2 successfully!'), ])); expect( processRunner.recordedCalls @@ -623,13 +620,11 @@ void main() { ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( output2, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2/plugin2', - 'Done!' + containsAllInOrder([ + contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Published plugin1 successfully!'), + contains('Running `pub publish ` in ${pluginDir2.path}...'), + contains('Published plugin2 successfully!'), ])); expect( processRunner.recordedCalls, @@ -642,7 +637,7 @@ void main() { }); test( - 'delete package will not trigger publish but exit the command successfully.', + 'delete package will not trigger publish but exit the command successfully!', () async { mockHttpResponses['plugin1'] = { 'name': 'plugin1', @@ -674,13 +669,13 @@ void main() { ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( output2, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n', - 'Packages released: plugin1', - 'Done!' + containsAllInOrder([ + contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Published plugin1 successfully!'), + contains( + 'The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n'), + contains('SKIPPING: package deleted'), + contains('skipped (with warning)'), ])); expect( processRunner.recordedCalls, @@ -724,14 +719,11 @@ void main() { expect( output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'The version 0.0.2 of plugin1 has already been published', - 'skip.', - 'The version 0.0.2 of plugin2 has already been published', - 'skip.', - 'Done!' + containsAllInOrder([ + contains('plugin1 0.0.2 has already been published'), + contains('SKIPPING: already published'), + contains('plugin2 0.0.2 has already been published'), + contains('SKIPPING: already published'), ])); expect( @@ -778,12 +770,10 @@ void main() { expect( output, containsAllInOrder([ - contains('The version 0.0.2 of plugin1 has already been published'), - contains( - 'However, the git release tag for this version (plugin1-v0.0.2) is not found.'), - contains('The version 0.0.2 of plugin2 has already been published'), - contains( - 'However, the git release tag for this version (plugin2-v0.0.2) is not found.'), + contains('plugin1 0.0.2 has already been published, ' + 'however the git release tag (plugin1-v0.0.2) was not found.'), + contains('plugin2 0.0.2 has already been published, ' + 'however the git release tag (plugin2-v0.0.2) was not found.'), ])); expect( processRunner.recordedCalls @@ -807,14 +797,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); - expect( - output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'No version updates in this commit.', - 'Done!' - ])); + expect(output, containsAllInOrder(['Ran for 0 package(s)'])); expect( processRunner.recordedCalls .map((ProcessCall call) => call.executable), @@ -838,14 +821,13 @@ void main() { expect( output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'Done!' + containsAllInOrder([ + contains( + 'SKIPPING: publishing flutter_plugin_tools via the tool is not supported') ])); expect( output.contains( - 'Running `pub publish ` in ${flutterPluginTools.path}...\n', + 'Running `pub publish ` in ${flutterPluginTools.path}...', ), isFalse); expect( From 6056abeab819d38936da33263511e96d9a0e54a6 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 31 Aug 2021 22:57:43 -0400 Subject: [PATCH 119/123] [flutter_plugin_tools] Fix build-examples for packages (#4248) The build-examples command was filtering what it attempted to build by plugin platform, which means it never does anything for non-plugin packages. flutter/packages has steps that run this command, which suggests it used to work and regressed at some point, but nobody noticed; this will re-enable those builds so that we are getting CI coverage that the examples in flutter/packages build. Mostly fixes https://github.com/flutter/flutter/issues/88435 (needs a flutter/packages tool pin roll to pick this up) --- script/tool/CHANGELOG.md | 4 + .../tool/lib/src/build_examples_command.dart | 66 +++++-- .../src/common/package_looping_command.dart | 9 +- script/tool/lib/src/common/plugin_utils.dart | 137 +++++++-------- script/tool/pubspec.yaml | 2 +- .../test/build_examples_command_test.dart | 162 ++++++++++++++++++ .../common/package_looping_command_test.dart | 58 ++++++- 7 files changed, 355 insertions(+), 83 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 2ef34c184b11..1f1da3551ef8 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.0+1 + +- Fixed `build-examples` to work for non-plugin packages. + ## 0.6.0 - Added Android native integration test support to `native-test`. diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index e441f61d5644..56c2f5c7dc87 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -117,39 +117,65 @@ class BuildExamplesCommand extends PackageLoopingCommand { Future runForPackage(RepositoryPackage package) async { final List errors = []; + final bool isPlugin = isFlutterPlugin(package); final Iterable<_PlatformDetails> requestedPlatforms = _platforms.entries .where( (MapEntry entry) => getBoolArg(entry.key)) .map((MapEntry entry) => entry.value); - final Set<_PlatformDetails> buildPlatforms = <_PlatformDetails>{}; - final Set<_PlatformDetails> unsupportedPlatforms = <_PlatformDetails>{}; - for (final _PlatformDetails platform in requestedPlatforms) { - if (pluginSupportsPlatform(platform.pluginPlatform, package, - variant: platform.pluginPlatformVariant)) { - buildPlatforms.add(platform); - } else { - unsupportedPlatforms.add(platform); - } + + // Platform support is checked at the package level for plugins; there is + // no package-level platform information for non-plugin packages. + final Set<_PlatformDetails> buildPlatforms = isPlugin + ? requestedPlatforms + .where((_PlatformDetails platform) => pluginSupportsPlatform( + platform.pluginPlatform, package, + variant: platform.pluginPlatformVariant)) + .toSet() + : requestedPlatforms.toSet(); + + String platformDisplayList(Iterable<_PlatformDetails> platforms) { + return platforms.map((_PlatformDetails p) => p.label).join(', '); } + if (buildPlatforms.isEmpty) { final String unsupported = requestedPlatforms.length == 1 ? '${requestedPlatforms.first.label} is not supported' - : 'None of [${requestedPlatforms.map((_PlatformDetails p) => p.label).join(',')}] are supported'; + : 'None of [${platformDisplayList(requestedPlatforms)}] are supported'; return PackageResult.skip('$unsupported by this plugin'); } - print('Building for: ' - '${buildPlatforms.map((_PlatformDetails platform) => platform.label).join(',')}'); + print('Building for: ${platformDisplayList(buildPlatforms)}'); + + final Set<_PlatformDetails> unsupportedPlatforms = + requestedPlatforms.toSet().difference(buildPlatforms); if (unsupportedPlatforms.isNotEmpty) { + final List skippedPlatforms = unsupportedPlatforms + .map((_PlatformDetails platform) => platform.label) + .toList(); + skippedPlatforms.sort(); print('Skipping unsupported platform(s): ' - '${unsupportedPlatforms.map((_PlatformDetails platform) => platform.label).join(',')}'); + '${skippedPlatforms.join(', ')}'); } print(''); + bool builtSomething = false; for (final RepositoryPackage example in package.getExamples()) { final String packageName = getRelativePosixPath(example.directory, from: packagesDir); for (final _PlatformDetails platform in buildPlatforms) { + // Repo policy is that a plugin must have examples configured for all + // supported platforms. For packages, just log and skip any requested + // platform that a package doesn't have set up. + if (!isPlugin && + !example.directory + .childDirectory(platform.flutterPlatformDirectory) + .existsSync()) { + print('Skipping ${platform.label} for $packageName; not supported.'); + continue; + } + + builtSomething = true; + String buildPlatform = platform.label; if (platform.label.toLowerCase() != platform.flutterBuildType) { buildPlatform += ' (${platform.flutterBuildType})'; @@ -162,6 +188,15 @@ class BuildExamplesCommand extends PackageLoopingCommand { } } + if (!builtSomething) { + if (isPlugin) { + errors.add('No examples found'); + } else { + return PackageResult.skip( + 'No examples found supporting requested platform(s).'); + } + } + return errors.isEmpty ? PackageResult.success() : PackageResult.fail(errors); @@ -235,6 +270,11 @@ class _PlatformDetails { /// The `flutter build` build type. final String flutterBuildType; + /// The Flutter platform directory name. + // In practice, this is the same as the plugin platform key for all platforms. + // If that changes, this can be adjusted. + String get flutterPlatformDirectory => pluginPlatform; + /// Any extra flags to pass to `flutter build`. final List extraBuildFlags; } diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 96dd881bfe00..973ac9995cb8 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -237,7 +237,14 @@ abstract class PackageLoopingCommand extends PluginCommand { continue; } - final PackageResult result = await runForPackage(entry.package); + PackageResult result; + try { + result = await runForPackage(entry.package); + } catch (e, stack) { + printError(e.toString()); + printError(stack.toString()); + result = PackageResult.fail(['Unhandled exception']); + } if (result.state == RunState.skipped) { final String message = '${indentation}SKIPPING: ${result.details.first}'; diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index 06af675e71ef..6cfe9928d689 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -17,6 +17,11 @@ enum PlatformSupport { federated, } +/// Returns true if [package] is a Flutter plugin. +bool isFlutterPlugin(RepositoryPackage package) { + return _readPluginPubspecSection(package) != null; +} + /// Returns true if [package] is a Flutter [platform] plugin. /// /// It checks this by looking for the following pattern in the pubspec: @@ -40,46 +45,43 @@ bool pluginSupportsPlatform( platform == kPlatformMacos || platform == kPlatformWindows || platform == kPlatformLinux); - try { - final YamlMap? platformEntry = - _readPlatformPubspecSectionForPlugin(platform, plugin); - if (platformEntry == null) { + + final YamlMap? platformEntry = + _readPlatformPubspecSectionForPlugin(platform, plugin); + if (platformEntry == null) { + return false; + } + + // If the platform entry is present, then it supports the platform. Check + // for required mode if specified. + if (requiredMode != null) { + final bool federated = platformEntry.containsKey('default_package'); + if (federated != (requiredMode == PlatformSupport.federated)) { return false; } + } - // If the platform entry is present, then it supports the platform. Check - // for required mode if specified. - if (requiredMode != null) { - final bool federated = platformEntry.containsKey('default_package'); - if (federated != (requiredMode == PlatformSupport.federated)) { + // If a variant is specified, check for that variant. + if (variant != null) { + const String variantsKey = 'supportedVariants'; + if (platformEntry.containsKey(variantsKey)) { + if (!(platformEntry['supportedVariants']! as YamlList) + .contains(variant)) { return false; } - } - - // If a variant is specified, check for that variant. - if (variant != null) { - const String variantsKey = 'supportedVariants'; - if (platformEntry.containsKey(variantsKey)) { - if (!(platformEntry['supportedVariants']! as YamlList) - .contains(variant)) { - return false; - } - } else { - // Platforms with variants have a default variant when unspecified for - // backward compatibility. Must match the flutter tool logic. - const Map defaultVariants = { - kPlatformWindows: platformVariantWin32, - }; - if (variant != defaultVariants[platform]) { - return false; - } + } else { + // Platforms with variants have a default variant when unspecified for + // backward compatibility. Must match the flutter tool logic. + const Map defaultVariants = { + kPlatformWindows: platformVariantWin32, + }; + if (variant != defaultVariants[platform]) { + return false; } } - - return true; - } on YamlException { - return false; } + + return true; } /// Returns true if [plugin] includes native code for [platform], as opposed to @@ -89,24 +91,18 @@ bool pluginHasNativeCodeForPlatform(String platform, RepositoryPackage plugin) { // Web plugins are always Dart-only. return false; } - try { - final YamlMap? platformEntry = - _readPlatformPubspecSectionForPlugin(platform, plugin); - if (platformEntry == null) { - return false; - } - // All other platforms currently use pluginClass for indicating the native - // code in the plugin. - final String? pluginClass = platformEntry['pluginClass'] as String?; - // TODO(stuartmorgan): Remove the check for 'none' once none of the plugins - // in the repository use that workaround. See - // https://github.com/flutter/flutter/issues/57497 for context. - return pluginClass != null && pluginClass != 'none'; - } on FileSystemException { - return false; - } on YamlException { + final YamlMap? platformEntry = + _readPlatformPubspecSectionForPlugin(platform, plugin); + if (platformEntry == null) { return false; } + // All other platforms currently use pluginClass for indicating the native + // code in the plugin. + final String? pluginClass = platformEntry['pluginClass'] as String?; + // TODO(stuartmorgan): Remove the check for 'none' once none of the plugins + // in the repository use that workaround. See + // https://github.com/flutter/flutter/issues/57497 for context. + return pluginClass != null && pluginClass != 'none'; } /// Returns the @@ -118,26 +114,33 @@ bool pluginHasNativeCodeForPlatform(String platform, RepositoryPackage plugin) { /// or the pubspec couldn't be read. YamlMap? _readPlatformPubspecSectionForPlugin( String platform, RepositoryPackage plugin) { - try { - final File pubspecFile = plugin.pubspecFile; - final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; - if (flutterSection == null) { - return null; - } - final YamlMap? pluginSection = flutterSection['plugin'] as YamlMap?; - if (pluginSection == null) { - return null; - } - final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; - if (platforms == null) { - return null; - } - return platforms[platform] as YamlMap?; - } on FileSystemException { + final YamlMap? pluginSection = _readPluginPubspecSection(plugin); + if (pluginSection == null) { + return null; + } + final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; + if (platforms == null) { + return null; + } + return platforms[platform] as YamlMap?; +} + +/// Returns the +/// flutter: +/// plugin: +/// platforms: +/// section from [plugin]'s pubspec.yaml, or null if either it is not present, +/// or the pubspec couldn't be read. +YamlMap? _readPluginPubspecSection(RepositoryPackage package) { + final File pubspecFile = package.pubspecFile; + if (!pubspecFile.existsSync()) { return null; - } on YamlException { + } + final YamlMap pubspecYaml = + loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; + if (flutterSection == null) { return null; } + return flutterSection['plugin'] as YamlMap?; } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 7c2bb0b3e3c0..adf62ca35a1a 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.6.0 +version: 0.6.0+1 dependencies: args: ^2.1.0 diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index a17107c18e27..d9cbad246d28 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -82,6 +82,35 @@ void main() { ])); }); + test('fails if a plugin has no examples', () async { + createFakePlugin('plugin', packagesDir, + examples: [], + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) + }); + + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ + MockProcess(exitCode: 1) // flutter packages get + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin:\n' + ' No examples found'), + ])); + }); + test('building for iOS when plugin is not set up for iOS results in no-op', () async { mockPlatform.isMacOS = true; @@ -517,5 +546,138 @@ void main() { pluginExampleDirectory.path), ])); }); + + test('logs skipped platforms', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + }); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--apk', '--ios', '--macos']); + + expect( + output, + containsAllInOrder([ + contains('Skipping unsupported platform(s): iOS, macOS'), + ]), + ); + }); + + group('packages', () { + test('builds when requested platform is supported by example', () async { + final Directory packageDirectory = createFakePackage( + 'package', packagesDir, isFlutter: true, extraFiles: [ + 'example/ios/Runner.xcodeproj/project.pbxproj' + ]); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('BUILDING package/example for iOS'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'build', + 'ios', + '--no-codesign', + ], + packageDirectory.childDirectory('example').path), + ])); + }); + + test('skips non-Flutter examples', () async { + createFakePackage('package', packagesDir, isFlutter: false); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('No examples found supporting requested platform(s).'), + ]), + ); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skips when there is no example', () async { + createFakePackage('package', packagesDir, + isFlutter: true, examples: []); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('No examples found supporting requested platform(s).'), + ]), + ); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip when example does not support requested platform', () async { + createFakePackage('package', packagesDir, + isFlutter: true, + extraFiles: ['example/linux/CMakeLists.txt']); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('Skipping iOS for package/example; not supported.'), + contains('No examples found supporting requested platform(s).'), + ]), + ); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('logs skipped platforms when only some are supported', () async { + final Directory packageDirectory = createFakePackage( + 'package', packagesDir, + isFlutter: true, + extraFiles: ['example/linux/CMakeLists.txt']); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--apk', '--linux']); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('Building for: Android, Linux'), + contains('Skipping Android for package/example; not supported.'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['build', 'linux'], + packageDirectory.childDirectory('example').path), + ])); + }); + }); }); } diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 721923ae9c6e..7cf03960a74d 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -36,6 +36,8 @@ const String _errorFile = 'errors'; const String _skipFile = 'skip'; // The filename within a package containing warnings to log during runForPackage. const String _warningFile = 'warnings'; +// The filename within a package indicating that it should throw. +const String _throwFile = 'throw'; void main() { late FileSystem fileSystem; @@ -117,7 +119,7 @@ void main() { expect(() => runCommand(command), throwsA(isA())); }); - test('does not stop looping', () async { + test('does not stop looping on error', () async { createFakePackage('package_a', packagesDir); final Directory failingPackage = createFakePlugin('package_b', packagesDir); @@ -141,6 +143,31 @@ void main() { '${_startHeadingColor}Running for package_c...$_endColor', ])); }); + + test('does not stop looping on exceptions', () async { + createFakePackage('package_a', packagesDir); + final Directory failingPackage = + createFakePlugin('package_b', packagesDir); + createFakePackage('package_c', packagesDir); + failingPackage.childFile(_throwFile).createSync(); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + Error? commandError; + final List output = + await runCommand(command, errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startHeadingColor}Running for package_b...$_endColor', + '${_startHeadingColor}Running for package_c...$_endColor', + ])); + }); }); group('package iteration', () { @@ -437,6 +464,31 @@ void main() { ])); }); + test('logs unhandled exceptions as errors', () async { + createFakePackage('package_a', packagesDir); + final Directory failingPackage = + createFakePlugin('package_b', packagesDir); + createFakePackage('package_c', packagesDir); + failingPackage.childFile(_throwFile).createSync(); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + Error? commandError; + final List output = + await runCommand(command, errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + '${_startErrorColor}Exception: Uh-oh$_endColor', + '${_startErrorColor}The following packages had errors:$_endColor', + '$_startErrorColor package_b:\n Unhandled exception$_endColor', + ])); + }); + test('prints run summary on success', () async { final Directory warnPackage1 = createFakePackage('package_a', packagesDir); @@ -657,6 +709,10 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { if (errorFile.existsSync()) { return PackageResult.fail(errorFile.readAsLinesSync()); } + final File throwFile = package.directory.childFile(_throwFile); + if (throwFile.existsSync()) { + throw Exception('Uh-oh'); + } return PackageResult.success(); } From 9ef18bcf6ce105a6668a8dd1fa61a1b7e1346d76 Mon Sep 17 00:00:00 2001 From: Yusuf <82844127+ydag@users.noreply.github.com> Date: Wed, 1 Sep 2021 09:11:03 +0200 Subject: [PATCH 120/123] [image_picker]Android update cache (#4124) --- .../image_picker/image_picker/CHANGELOG.md | 4 ++ .../plugins/imagepicker/ImagePickerCache.java | 25 ++++++++---- .../imagepicker/ImagePickerDelegate.java | 39 +++++++++++-------- .../imagepicker/ImagePickerDelegateTest.java | 34 ++++++++++++++++ .../image_picker/example/lib/main.dart | 1 + .../image_picker/image_picker/pubspec.yaml | 4 +- .../image_picker/test/image_picker_test.dart | 18 +++++++++ .../method_channel_image_picker.dart | 1 - 8 files changed, 100 insertions(+), 26 deletions(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index a9255976c526..5dc260993773 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.4 + +* Update `ImagePickerCache` to cache multiple files. + ## 0.8.3+3 * Fix pickImage not returning a value on iOS when dismissing PHPicker sheet by swiping. diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java index 3df0a4108b5c..983dbabf66c3 100644 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java +++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java @@ -10,12 +10,16 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import io.flutter.plugin.common.MethodCall; +import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; class ImagePickerCache { static final String MAP_KEY_PATH = "path"; + static final String MAP_KEY_PATH_LIST = "pathList"; static final String MAP_KEY_MAX_WIDTH = "maxWidth"; static final String MAP_KEY_MAX_HEIGHT = "maxHeight"; static final String MAP_KEY_IMAGE_QUALITY = "imageQuality"; @@ -50,7 +54,8 @@ class ImagePickerCache { } void saveTypeWithMethodCallName(String methodCallName) { - if (methodCallName.equals(ImagePickerPlugin.METHOD_CALL_IMAGE)) { + if (methodCallName.equals(ImagePickerPlugin.METHOD_CALL_IMAGE) + | methodCallName.equals(ImagePickerPlugin.METHOD_CALL_MULTI_IMAGE)) { setType("image"); } else if (methodCallName.equals(ImagePickerPlugin.METHOD_CALL_VIDEO)) { setType("video"); @@ -99,11 +104,13 @@ String retrievePendingCameraMediaUriPath() { } void saveResult( - @Nullable String path, @Nullable String errorCode, @Nullable String errorMessage) { + @Nullable ArrayList path, @Nullable String errorCode, @Nullable String errorMessage) { + Set imageSet = new HashSet<>(); + imageSet.addAll(path); SharedPreferences.Editor editor = prefs.edit(); if (path != null) { - editor.putString(FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY, path); + editor.putStringSet(FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY, imageSet); } if (errorCode != null) { editor.putString(SHARED_PREFERENCE_ERROR_CODE_KEY, errorCode); @@ -121,12 +128,17 @@ void clear() { Map getCacheMap() { Map resultMap = new HashMap<>(); + ArrayList pathList = new ArrayList<>(); boolean hasData = false; if (prefs.contains(FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY)) { - final String imagePathValue = prefs.getString(FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY, ""); - resultMap.put(MAP_KEY_PATH, imagePathValue); - hasData = true; + final Set imagePathList = + prefs.getStringSet(FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY, null); + if (imagePathList != null) { + pathList.addAll(imagePathList); + resultMap.put(MAP_KEY_PATH_LIST, pathList); + hasData = true; + } } if (prefs.contains(SHARED_PREFERENCE_ERROR_CODE_KEY)) { @@ -159,7 +171,6 @@ Map getCacheMap() { resultMap.put(MAP_KEY_IMAGE_QUALITY, 100); } } - return resultMap; } } diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index dbd0f70af936..a60c1f173041 100644 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -217,17 +217,21 @@ void saveStateBeforeResult() { void retrieveLostImage(MethodChannel.Result result) { Map resultMap = cache.getCacheMap(); - String path = (String) resultMap.get(cache.MAP_KEY_PATH); - if (path != null) { - Double maxWidth = (Double) resultMap.get(cache.MAP_KEY_MAX_WIDTH); - Double maxHeight = (Double) resultMap.get(cache.MAP_KEY_MAX_HEIGHT); - int imageQuality = - resultMap.get(cache.MAP_KEY_IMAGE_QUALITY) == null - ? 100 - : (int) resultMap.get(cache.MAP_KEY_IMAGE_QUALITY); - - String newPath = imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality); - resultMap.put(cache.MAP_KEY_PATH, newPath); + ArrayList pathList = (ArrayList) resultMap.get(cache.MAP_KEY_PATH_LIST); + ArrayList newPathList = new ArrayList<>(); + if (pathList != null) { + for (String path : pathList) { + Double maxWidth = (Double) resultMap.get(cache.MAP_KEY_MAX_WIDTH); + Double maxHeight = (Double) resultMap.get(cache.MAP_KEY_MAX_HEIGHT); + int imageQuality = + resultMap.get(cache.MAP_KEY_IMAGE_QUALITY) == null + ? 100 + : (int) resultMap.get(cache.MAP_KEY_IMAGE_QUALITY); + + newPathList.add(imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality)); + } + resultMap.put(cache.MAP_KEY_PATH_LIST, newPathList); + resultMap.put(cache.MAP_KEY_PATH, newPathList.get(newPathList.size() - 1)); } if (resultMap.isEmpty()) { result.success(null); @@ -558,6 +562,7 @@ public void onPathReady(String path) { private void handleMultiImageResult( ArrayList paths, boolean shouldDeleteOriginalIfScaled) { if (methodCall != null) { + ArrayList finalPath = new ArrayList<>(); for (int i = 0; i < paths.size(); i++) { String finalImagePath = getResizedImagePath(paths.get(i)); @@ -567,8 +572,10 @@ private void handleMultiImageResult( && shouldDeleteOriginalIfScaled) { new File(paths.get(i)).delete(); } - paths.set(i, finalImagePath); + finalPath.add(i, finalImagePath); } + finishWithListSuccess(finalPath); + } else { finishWithListSuccess(paths); } } @@ -615,7 +622,9 @@ private boolean setPendingMethodCallAndResult( private void finishWithSuccess(String imagePath) { if (pendingResult == null) { - cache.saveResult(imagePath, null, null); + ArrayList pathList = new ArrayList<>(); + pathList.add(imagePath); + cache.saveResult(pathList, null, null); return; } pendingResult.success(imagePath); @@ -624,9 +633,7 @@ private void finishWithSuccess(String imagePath) { private void finishWithListSuccess(ArrayList imagePaths) { if (pendingResult == null) { - for (String imagePath : imagePaths) { - cache.saveResult(imagePath, null, null); - } + cache.saveResult(imagePaths, null, null); return; } pendingResult.success(imagePaths); diff --git a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index ebd58d05fee4..d2ee7b0b7d61 100644 --- a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java +++ b/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -5,10 +5,12 @@ package io.flutter.plugins.imagepicker; import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -26,9 +28,13 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; @@ -368,6 +374,34 @@ public void onActivityResult_WhenImageTakenWithCamera_AndNoResizeNeeded_Finishes verifyNoMoreInteractions(mockResult); } + @Test + public void + retrieveLostImage_ShouldBeAbleToReturnLastItemFromResultMapWhenSingleFileIsRecovered() { + Map resultMap = new HashMap<>(); + ArrayList pathList = new ArrayList<>(); + pathList.add("/example/first_item"); + pathList.add("/example/last_item"); + resultMap.put("pathList", pathList); + + when(mockImageResizer.resizeImageIfNeeded(pathList.get(0), null, null, 100)) + .thenReturn(pathList.get(0)); + when(mockImageResizer.resizeImageIfNeeded(pathList.get(1), null, null, 100)) + .thenReturn(pathList.get(1)); + when(cache.getCacheMap()).thenReturn(resultMap); + + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + ImagePickerDelegate mockDelegate = createDelegate(); + + ArgumentCaptor> valueCapture = ArgumentCaptor.forClass(Map.class); + + doNothing().when(mockResult).success(valueCapture.capture()); + + mockDelegate.retrieveLostImage(mockResult); + + assertEquals("/example/last_item", valueCapture.getValue().get("path")); + } + private ImagePickerDelegate createDelegate() { return new ImagePickerDelegate( mockActivity, diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 2d5fd9aee4a7..0f5ba76db6df 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -226,6 +226,7 @@ class _MyHomePageState extends State { isVideo = false; setState(() { _imageFile = response.file; + _imageFileList = response.files; }); } } else { diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 4becca930261..3bbcfe99882e 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.3+3 +version: 0.8.4 environment: sdk: ">=2.12.0 <3.0.0" @@ -25,7 +25,7 @@ dependencies: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 image_picker_for_web: ^2.1.0 - image_picker_platform_interface: ^2.2.0 + image_picker_platform_interface: ^2.3.0 dev_dependencies: flutter_test: diff --git a/packages/image_picker/image_picker/test/image_picker_test.dart b/packages/image_picker/image_picker/test/image_picker_test.dart index 960dfe6917ea..10bc64082aca 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.dart @@ -315,6 +315,24 @@ void main() { expect(response.file!.path, '/example/path'); }); + test('retrieveLostData should successfully retrieve multiple files', + () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path1', + 'pathList': ['/example/path0', '/example/path1'], + }; + }); + + final LostDataResponse response = await picker.retrieveLostData(); + expect(response.type, RetrieveType.image); + expect(response.file, isNotNull); + expect(response.file!.path, '/example/path1'); + expect(response.files!.first.path, '/example/path0'); + expect(response.files!.length, 2); + }); + test('retrieveLostData get error response', () async { channel.setMockMethodCallHandler((MethodCall methodCall) async { return { diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart index 292cb814ddeb..b02284e957fa 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart @@ -259,7 +259,6 @@ class MethodChannelImagePicker extends ImagePickerPlatform { final pathList = result['pathList']; if (pathList != null) { pickedFileList = []; - // In this case, multiRetrieve is invoked. for (String path in pathList) { pickedFileList.add(XFile(path)); } From df75b01c5a2d860fa22886032dda12c5dd5ce27c Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 1 Sep 2021 14:18:22 -0400 Subject: [PATCH 121/123] [flutter_plugin_tools] Add Linux support to native-test (#4294) - Adds a minimal unit test to url_launcher_linux as a proof of concept. This uses almost exactly the same CMake structure as the Windows version that was added recently. - Adds Linux support for unit tests to `native-test`, sharing almost all of the existing Windows codepath. - Fixes the fact that it it was running the debug version of the unit tests, but `build-examples` only builds release. (On other platforms we run debug unit tests, but on those platforms the test command internally builds the requested unit tests, so the mismatch doesn't matter.) - Enables the new test in CI. Also opportunistically fixes some documentation in `native_test_command.dart` that wasn't updated as more platform support was added. Linux portion of https://github.com/flutter/flutter/issues/82445 --- .cirrus.yml | 2 + .../example/linux/CMakeLists.txt | 3 + .../example/linux/flutter/CMakeLists.txt | 3 +- .../url_launcher_linux/linux/CMakeLists.txt | 47 +++++- .../linux/test/url_launcher_linux_test.cc | 57 +++++++ .../linux/url_launcher_plugin.cc | 4 +- .../linux/url_launcher_plugin_private.h | 14 ++ script/tool/CHANGELOG.md | 4 + script/tool/lib/src/native_test_command.dart | 37 ++++- .../tool/test/native_test_command_test.dart | 156 +++++++++++++++++- 10 files changed, 312 insertions(+), 15 deletions(-) create mode 100644 packages/url_launcher/url_launcher_linux/linux/test/url_launcher_linux_test.cc create mode 100644 packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin_private.h diff --git a/.cirrus.yml b/.cirrus.yml index d830a2a15913..10d668d8d1d7 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -127,6 +127,8 @@ task: build_script: - flutter config --enable-linux-desktop - ./script/tool_runner.sh build-examples --linux + native_test_script: + - ./script/tool_runner.sh native-test --linux --no-integration drive_script: - xvfb-run ./script/tool_runner.sh drive-examples --linux diff --git a/packages/url_launcher/url_launcher_linux/example/linux/CMakeLists.txt b/packages/url_launcher/url_launcher_linux/example/linux/CMakeLists.txt index 0236a8806654..1758aac03b0d 100644 --- a/packages/url_launcher/url_launcher_linux/example/linux/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_linux/example/linux/CMakeLists.txt @@ -43,6 +43,9 @@ target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) add_dependencies(${BINARY_NAME} flutter_assemble) +# Enable the test target. +set(include_url_launcher_linux_tests TRUE) + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) diff --git a/packages/url_launcher/url_launcher_linux/example/linux/flutter/CMakeLists.txt b/packages/url_launcher/url_launcher_linux/example/linux/flutter/CMakeLists.txt index 94f43ff7fa6a..33fd5801e713 100644 --- a/packages/url_launcher/url_launcher_linux/example/linux/flutter/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_linux/example/linux/flutter/CMakeLists.txt @@ -78,7 +78,8 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - linux-x64 ${CMAKE_BUILD_TYPE} + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" diff --git a/packages/url_launcher/url_launcher_linux/linux/CMakeLists.txt b/packages/url_launcher/url_launcher_linux/linux/CMakeLists.txt index 1403d0cbc9e4..b3f4a22b053d 100644 --- a/packages/url_launcher/url_launcher_linux/linux/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_linux/linux/CMakeLists.txt @@ -4,9 +4,13 @@ project(${PROJECT_NAME} LANGUAGES CXX) set(PLUGIN_NAME "${PROJECT_NAME}_plugin") -add_library(${PLUGIN_NAME} SHARED +list(APPEND PLUGIN_SOURCES "url_launcher_plugin.cc" ) + +add_library(${PLUGIN_NAME} SHARED + ${PLUGIN_SOURCES} +) apply_standard_settings(${PLUGIN_NAME}) set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) @@ -15,3 +19,44 @@ target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") target_link_libraries(${PLUGIN_NAME} PRIVATE flutter) target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK) + + +# === Tests === + +if (${include_${PROJECT_NAME}_tests}) +if(${CMAKE_VERSION} VERSION_LESS "3.11.0") +message("Unit tests require CMake 3.11.0 or later") +else() +set(TEST_RUNNER "${PROJECT_NAME}_test") +enable_testing() +# TODO(stuartmorgan): Consider using a single shared, pre-checked-in googletest +# instance rather than downloading for each plugin. This approach makes sense +# for a template, but not for a monorepo with many plugins. +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/release-1.11.0.zip +) +# Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +# Disable install commands for gtest so it doesn't end up in the bundle. +set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) + +FetchContent_MakeAvailable(googletest) + +# The plugin's exported API is not very useful for unit testing, so build the +# sources directly into the test binary rather than using the shared library. +add_executable(${TEST_RUNNER} + test/url_launcher_linux_test.cc + ${PLUGIN_SOURCES} +) +apply_standard_settings(${TEST_RUNNER}) +target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(${TEST_RUNNER} PRIVATE flutter) +target_link_libraries(${TEST_RUNNER} PRIVATE PkgConfig::GTK) +target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) + +include(GoogleTest) +gtest_discover_tests(${TEST_RUNNER}) +endif() # CMake version check +endif() # include_${PROJECT_NAME}_tests diff --git a/packages/url_launcher/url_launcher_linux/linux/test/url_launcher_linux_test.cc b/packages/url_launcher/url_launcher_linux/linux/test/url_launcher_linux_test.cc new file mode 100644 index 000000000000..e655638c4ed7 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/linux/test/url_launcher_linux_test.cc @@ -0,0 +1,57 @@ +// 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. +#include +#include +#include + +#include +#include + +#include "include/url_launcher_linux/url_launcher_plugin.h" +#include "url_launcher_plugin_private.h" + +namespace url_launcher_plugin { +namespace test { + +TEST(UrlLauncherPlugin, CanLaunchSuccess) { + g_autoptr(FlValue) args = fl_value_new_map(); + fl_value_set_string_take(args, "url", + fl_value_new_string("https://flutter.dev")); + FlMethodResponse* response = can_launch(nullptr, args); + ASSERT_NE(response, nullptr); + ASSERT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + g_autoptr(FlValue) expected = fl_value_new_bool(true); + EXPECT_TRUE(fl_value_equal(fl_method_success_response_get_result( + FL_METHOD_SUCCESS_RESPONSE(response)), + expected)); +} + +TEST(UrlLauncherPlugin, CanLaunchFailureUnhandled) { + g_autoptr(FlValue) args = fl_value_new_map(); + fl_value_set_string_take(args, "url", fl_value_new_string("madeup:scheme")); + FlMethodResponse* response = can_launch(nullptr, args); + ASSERT_NE(response, nullptr); + ASSERT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + g_autoptr(FlValue) expected = fl_value_new_bool(false); + EXPECT_TRUE(fl_value_equal(fl_method_success_response_get_result( + FL_METHOD_SUCCESS_RESPONSE(response)), + expected)); +} + +// For consistency with the established mobile implementations, +// an invalid URL should return false, not an error. +TEST(UrlLauncherPlugin, CanLaunchFailureInvalidUrl) { + g_autoptr(FlValue) args = fl_value_new_map(); + fl_value_set_string_take(args, "url", fl_value_new_string("")); + FlMethodResponse* response = can_launch(nullptr, args); + ASSERT_NE(response, nullptr); + ASSERT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + g_autoptr(FlValue) expected = fl_value_new_bool(false); + EXPECT_TRUE(fl_value_equal(fl_method_success_response_get_result( + FL_METHOD_SUCCESS_RESPONSE(response)), + expected)); +} + +} // namespace test +} // namespace url_launcher_plugin diff --git a/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin.cc b/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin.cc index 6e10607dd14e..d3f454ee7198 100644 --- a/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin.cc +++ b/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin.cc @@ -9,6 +9,8 @@ #include +#include "url_launcher_plugin_private.h" + // See url_launcher_channel.dart for documentation. const char kChannelName[] = "plugins.flutter.io/url_launcher"; const char kBadArgumentsError[] = "Bad Arguments"; @@ -44,7 +46,7 @@ static gchar* get_url(FlValue* args, GError** error) { } // Called to check if a URL can be launched. -static FlMethodResponse* can_launch(FlUrlLauncherPlugin* self, FlValue* args) { +FlMethodResponse* can_launch(FlUrlLauncherPlugin* self, FlValue* args) { g_autoptr(GError) error = nullptr; g_autofree gchar* url = get_url(args, &error); if (url == nullptr) { diff --git a/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin_private.h b/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin_private.h new file mode 100644 index 000000000000..cde5242a8f47 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin_private.h @@ -0,0 +1,14 @@ +// 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. + +#include + +#include "include/url_launcher_linux/url_launcher_plugin.h" + +// TODO(stuartmorgan): Remove this private header and change the below back to +// a static function once https://github.com/flutter/flutter/issues/88724 +// is fixed, and test through the public API instead. + +// Handles the canLaunch method call. +FlMethodResponse* can_launch(FlUrlLauncherPlugin* self, FlValue* args); diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 1f1da3551ef8..9b6bbb1f71cc 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +- `native-test` now supports `--linux` for unit tests. + ## 0.6.0+1 - Fixed `build-examples` to work for non-plugin packages. diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 5120ad10b872..e50878db7906 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -21,7 +21,9 @@ const String _iosDestinationFlag = 'ios-destination'; const int _exitNoIosSimulators = 3; /// The command to run native tests for plugins: -/// - iOS and macOS: XCTests (XCUnitTest and XCUITest) in plugins. +/// - iOS and macOS: XCTests (XCUnitTest and XCUITest) +/// - Android: JUnit tests +/// - Windows and Linux: GoogleTest tests class NativeTestCommand extends PackageLoopingCommand { /// Creates an instance of the test command. NativeTestCommand( @@ -39,6 +41,7 @@ class NativeTestCommand extends PackageLoopingCommand { ); argParser.addFlag(kPlatformAndroid, help: 'Runs Android tests'); argParser.addFlag(kPlatformIos, help: 'Runs iOS tests'); + argParser.addFlag(kPlatformLinux, help: 'Runs Linux tests'); argParser.addFlag(kPlatformMacos, help: 'Runs macOS tests'); argParser.addFlag(kPlatformWindows, help: 'Runs Windows tests'); @@ -63,9 +66,11 @@ class NativeTestCommand extends PackageLoopingCommand { Runs native unit tests and native integration tests. Currently supported platforms: -- Android (unit tests only) +- Android - iOS: requires 'xcrun' to be in your path. +- Linux (unit tests only) - macOS: requires 'xcrun' to be in your path. +- Windows (unit tests only) The example app(s) must be built for all targeted platforms before running this command. @@ -80,6 +85,7 @@ this command. _platforms = { kPlatformAndroid: _PlatformDetails('Android', _testAndroid), kPlatformIos: _PlatformDetails('iOS', _testIos), + kPlatformLinux: _PlatformDetails('Linux', _testLinux), kPlatformMacos: _PlatformDetails('macOS', _testMacOS), kPlatformWindows: _PlatformDetails('Windows', _testWindows), }; @@ -103,6 +109,11 @@ this command. 'See https://github.com/flutter/flutter/issues/70233.'); } + if (getBoolArg(kPlatformLinux) && getBoolArg(_integrationTestFlag)) { + logWarning('This command currently only supports unit tests for Linux. ' + 'See https://github.com/flutter/flutter/issues/70235.'); + } + // iOS-specific run-level state. if (_requestedPlatforms.contains('ios')) { String destination = getStringArg(_iosDestinationFlag); @@ -418,6 +429,21 @@ this command. buildDirectoryName: 'windows', isTestBinary: isTestBinary); } + Future<_PlatformResult> _testLinux( + RepositoryPackage plugin, _TestMode mode) async { + if (mode.integrationOnly) { + return _PlatformResult(RunState.skipped); + } + + bool isTestBinary(File file) { + return file.basename.endsWith('_test') || + file.basename.endsWith('_tests'); + } + + return _runGoogleTestTests(plugin, + buildDirectoryName: 'linux', isTestBinary: isTestBinary); + } + /// Finds every file in the [buildDirectoryName] subdirectory of [plugin]'s /// build directory for which [isTestBinary] is true, and runs all of them, /// returning the overall result. @@ -442,10 +468,11 @@ this command. .whereType() .where(isTestBinary) .where((File file) { - // Only run the debug build of the unit tests, to avoid running the - // same tests multiple times. + // Only run the release build of the unit tests, to avoid running the + // same tests multiple times. Release is used rather than debug since + // `build-examples` builds release versions. final List components = path.split(file.path); - return components.contains('debug') || components.contains('Debug'); + return components.contains('release') || components.contains('Release'); })); } diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index 3613a808d9b8..d1ab11f6e50d 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -736,6 +736,147 @@ void main() { }); }); + group('Linux', () { + test('runs unit tests', () async { + const String testBinaryRelativePath = + 'build/linux/foo/release/bar/plugin_test'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$testBinaryRelativePath' + ], platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }); + + final File testBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...testBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--linux', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test...'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(testBinary.path, const [], null), + ])); + }); + + test('only runs release unit tests', () async { + const String debugTestBinaryRelativePath = + 'build/linux/foo/debug/bar/plugin_test'; + const String releaseTestBinaryRelativePath = + 'build/linux/foo/release/bar/plugin_test'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$debugTestBinaryRelativePath', + 'example/$releaseTestBinaryRelativePath' + ], platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }); + + final File releaseTestBinary = childFileWithSubcomponents( + pluginDirectory, + ['example', ...releaseTestBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--linux', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test...'), + contains('No issues found!'), + ]), + ); + + // Only the release version should be run. + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(releaseTestBinary.path, const [], null), + ])); + }); + + test('fails if there are no unit tests', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--linux', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No test binaries found.'), + ]), + ); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('fails if a unit test fails', () async { + const String testBinaryRelativePath = + 'build/linux/foo/release/bar/plugin_test'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$testBinaryRelativePath' + ], platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }); + + final File testBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...testBinaryRelativePath.split('/')]); + + processRunner.mockProcessesForExecutable[testBinary.path] = + [MockProcess(exitCode: 1)]; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--linux', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running plugin_test...'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(testBinary.path, const [], null), + ])); + }); + }); + // Tests behaviors of implementation that is shared between iOS and macOS. group('iOS/macOS', () { test('fails if xcrun fails', () async { @@ -1352,7 +1493,7 @@ void main() { group('Windows', () { test('runs unit tests', () async { const String testBinaryRelativePath = - 'build/windows/foo/Debug/bar/plugin_test.exe'; + 'build/windows/foo/Release/bar/plugin_test.exe'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' @@ -1384,7 +1525,7 @@ void main() { ])); }); - test('only runs debug unit tests', () async { + test('only runs release unit tests', () async { const String debugTestBinaryRelativePath = 'build/windows/foo/Debug/bar/plugin_test.exe'; const String releaseTestBinaryRelativePath = @@ -1397,8 +1538,9 @@ void main() { kPlatformWindows: const PlatformDetails(PlatformSupport.inline), }); - final File debugTestBinary = childFileWithSubcomponents(pluginDirectory, - ['example', ...debugTestBinaryRelativePath.split('/')]); + final File releaseTestBinary = childFileWithSubcomponents( + pluginDirectory, + ['example', ...releaseTestBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ 'native-test', @@ -1414,11 +1556,11 @@ void main() { ]), ); - // Only the debug version should be run. + // Only the release version should be run. expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(debugTestBinary.path, const [], null), + ProcessCall(releaseTestBinary.path, const [], null), ])); }); @@ -1450,7 +1592,7 @@ void main() { test('fails if a unit test fails', () async { const String testBinaryRelativePath = - 'build/windows/foo/Debug/bar/plugin_test.exe'; + 'build/windows/foo/Release/bar/plugin_test.exe'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' From bafc4ee36021ab80113c6955f3f4271950ec63b2 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Wed, 1 Sep 2021 13:11:03 -0700 Subject: [PATCH 122/123] Add a way to opt a file out of Dart formatting (#4292) --- script/tool/CHANGELOG.md | 4 ++- script/tool/lib/src/format_command.dart | 20 ++++++++++++ script/tool/pubspec.yaml | 2 +- script/tool/test/format_command_test.dart | 37 +++++++++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 9b6bbb1f71cc..098e57a8c62d 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,6 +1,8 @@ -## NEXT +## 0.7.0 - `native-test` now supports `--linux` for unit tests. +- Formatting now skips Dart files that contain a line that exactly + matches the string `// This file is hand-formatted.`. ## 0.6.0+1 diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index d09a94b1aefe..f24a99436c87 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -206,7 +206,27 @@ class FormatCommand extends PluginCommand { final String fromPath = relativeTo.path; + // Dart files are allowed to have a pragma to disable auto-formatting. This + // was added because Hixie hurts when dealing with what dartfmt does to + // artisanally-formatted Dart, while Stuart gets really frustrated when + // dealing with PRs from newer contributors who don't know how to make Dart + // readable. After much discussion, it was decided that files in the plugins + // and packages repos that really benefit from hand-formatting (e.g. files + // with large blobs of hex literals) could be opted-out of the requirement + // that they be autoformatted, so long as the code's owner was willing to + // bear the cost of this during code reviews. + // In the event that code ownership moves to someone who does not hold the + // same views as the original owner, the pragma can be removed and the file + // auto-formatted. + const String handFormattedExtension = '.dart'; + const String handFormattedPragma = '// This file is hand-formatted.'; + return files + .where((File file) { + // See comment above near [handFormattedPragma]. + return path.extension(file.path) != handFormattedExtension || + !file.readAsLinesSync().contains(handFormattedPragma); + }) .map((File file) => path.relative(file.path, from: fromPath)) .where((String path) => // Ignore files in build/ directories (e.g., headers of frameworks) diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index adf62ca35a1a..2569e0ede870 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.6.0+1 +version: 0.7.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index e2bf1e3e6e8e..d278bb2940b8 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -8,6 +8,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/file_utils.dart'; import 'package:flutter_plugin_tools/src/format_command.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -106,6 +107,42 @@ void main() { ])); }); + test('does not format .dart files with pragma', () async { + const List formattedFiles = [ + 'lib/a.dart', + 'lib/src/b.dart', + 'lib/src/c.dart', + ]; + const String unformattedFile = 'lib/src/d.dart'; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: [ + ...formattedFiles, + unformattedFile, + ], + ); + + final p.Context posixContext = p.posix; + childFileWithSubcomponents(pluginDir, posixContext.split(unformattedFile)) + .writeAsStringSync( + '// copyright bla bla\n// This file is hand-formatted.\ncode...'); + + await runCapturingPrint(runner, ['format']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + [ + 'format', + ..._getPackagesDirRelativePaths(pluginDir, formattedFiles) + ], + packagesDir.path), + ])); + }); + test('fails if flutter format fails', () async { const List files = [ 'lib/a.dart', From 5d1ed48dfc8e0d6a9fa9afe137e18071c190e1b8 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 2 Sep 2021 13:11:04 +0200 Subject: [PATCH 123/123] Ensure setExposureOffset returns new value on Android (#4301) --- packages/camera/camera/CHANGELOG.md | 4 ++++ .../src/main/java/io/flutter/plugins/camera/Camera.java | 2 +- .../test/java/io/flutter/plugins/camera/CameraTest.java | 4 +++- packages/camera/camera/example/lib/main.dart | 7 +++++++ packages/camera/camera/pubspec.yaml | 2 +- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 5a3a1bf251d7..b141fab62595 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.2+2 + +* Ensure that setting the exposure offset returns the new offset value on Android. + ## 0.9.2+1 * Fixed camera controller throwing an exception when being replaced in the preview widget. diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index c036c1c7e9d3..4601e7d34d69 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -906,7 +906,7 @@ public void setExposureOffset(@NonNull final Result result, double offset) { exposureOffsetFeature.updateBuilder(previewRequestBuilder); refreshPreviewCaptureSession( - () -> result.success(null), + () -> result.success(exposureOffsetFeature.getValue()), (code, message) -> result.error("setExposureOffsetFailed", "Could not set exposure offset.", null)); } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java index 5431df0df636..fbed28bc11fc 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -687,11 +687,13 @@ public void setExposureOffset_shouldUpdateExposureOffsetFeature() { mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + when(mockExposureOffsetFeature.getValue()).thenReturn(1.0); + camera.setExposureOffset(mockResult, 1.0); verify(mockExposureOffsetFeature, times(1)).setValue(1.0); verify(mockResult, never()).error(any(), any(), any()); - verify(mockResult, times(1)).success(null); + verify(mockResult, times(1)).success(1.0); } @Test diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index a8067001aae5..c0e90eefa3ab 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -399,6 +399,13 @@ class _CameraExampleHomeState extends State onSetExposureModeButtonPressed(ExposureMode.locked) : null, ), + TextButton( + child: Text('RESET OFFSET'), + style: styleLocked, + onPressed: controller != null + ? () => controller!.setExposureOffset(0.0) + : null, + ), ], ), Center( diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 400b8c03f44a..582a830ebb4c 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.2+1 +version: 0.9.2+2 environment: sdk: ">=2.12.0 <3.0.0"