diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml index 2c9c96cbe1a1c..8a18797fd9d6e 100644 --- a/.github/auto_assign.yml +++ b/.github/auto_assign.yml @@ -17,7 +17,6 @@ reviewers: - GaryQian - jason-simmons - iskakaushik - - cbracken - flar # A number of reviewers added to the pull request diff --git a/DEPS b/DEPS index f31ad065468c0..63b4509d5d270 100644 --- a/DEPS +++ b/DEPS @@ -26,7 +26,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', # OCMock is for testing only so there is no google clone 'ocmock_git': 'https://github.com/erikdoe/ocmock.git', - 'skia_revision': '50daeddf396f2f1cbb3a4a70ec785f8e3d4dca66', + 'skia_revision': '632db1c742121f6176aa4627bac2ab1dc85cde5a', # When updating the Dart revision, ensure that all entries that are # dependencies of Dart are also updated to match the entries in the @@ -34,7 +34,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/master/DEPS. # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': '9fcac032b669a772050fea629acf9732d0d80a7b', + 'dart_revision': 'd8eb844e5d8f8c23166cb6877bcbe501d889ba38', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py @@ -354,7 +354,7 @@ deps = { Var('dart_git') + '/package_config.git@9c586d04bd26fef01215fd10e7ab96a3050cfa64', 'src/third_party/dart/tools/sdks': - {'packages': [{'version': 'version:2.9.0-16.0.dev', 'package': 'dart/dart-sdk/${{platform}}'}], 'dep_type': 'cipd'}, + {'packages': [{'version': 'version:2.9.0-18.0.dev', 'package': 'dart/dart-sdk/${{platform}}'}], 'dep_type': 'cipd'}, # WARNING: end of dart dependencies list that is cleaned up automatically - see create_updated_flutter_deps.py. @@ -521,7 +521,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/mac-amd64', - 'version': 'thz2_oecn3_6i9chmPVP3cC5_WqCwF80ttrY9vqK4jYC' + 'version': 'l2ubU17oZ4OMTqGo24etGpB7DBv7fY6qqTlAk0IZCB0C' } ], 'condition': 'host_os == "mac"', @@ -541,7 +541,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/linux-amd64', - 'version': 'gaCdqe5pvWAil0qJ9M7V7TWshgYOn7zOvYyKzM0wzwQC' + 'version': 'GqEvVysSbeMClo7IZMsJWgJQZiDp9ZibF_n3cCpmaBEC' } ], 'condition': 'host_os == "linux"', diff --git a/README.md b/README.md index eb16aa51bb3e0..d3c5dcf37a452 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ toolchain. Most developers will interact with Flutter via the [Flutter Framework](https://github.com/flutter/flutter), which provides a modern, reactive framework, and a rich set of platform, layout and foundation widgets. +If you want to run/contribute to Flutter Web engine, more tooling can be +found at [felt](https://github.com/flutter/engine/tree/master/lib/web_ui/dev#whats-felt). +This is a tool written to make web engine development experience easy. + If you are new to Flutter, then you will find more general information on the Flutter project, including tutorials and samples, on our Web site at [Flutter.dev](https://flutter.dev). For specific information diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 4cd541e0fed4f..53ec4d21c6b56 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -690,6 +690,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Flutt FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterEngineProvider.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSplashView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java diff --git a/ci/licenses_golden/licenses_fuchsia b/ci/licenses_golden/licenses_fuchsia index a15a02b2d13f9..9d137b1221f48 100644 --- a/ci/licenses_golden/licenses_fuchsia +++ b/ci/licenses_golden/licenses_fuchsia @@ -1,4 +1,4 @@ -Signature: 3faf3cde076e4318d67e988eb33ab900 +Signature: 249cd748150db631d8b4aaa058320686 UNUSED LICENSES: @@ -1485,7 +1485,6 @@ FILE: ../../../fuchsia/sdk/linux/pkg/fdio/include/lib/fdio/fd.h FILE: ../../../fuchsia/sdk/linux/pkg/fdio/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/fidl-async/meta.json FILE: ../../../fuchsia/sdk/linux/pkg/fidl/meta.json -FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/include/lib/fidl/envelope_frames.h FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/include/lib/fidl/internal_callable_traits.h FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/include/lib/fidl/txn_header.h FILE: ../../../fuchsia/sdk/linux/pkg/fidl_base/include/lib/fidl/visitor.h @@ -3158,6 +3157,8 @@ FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.castwindow/window.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component/constants.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component/error.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.component/types.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.diagnostics/interest.fidl +FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.diagnostics/severity.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.factory.wlan/iovar.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/crash_register.fidl FILE: ../../../fuchsia/sdk/linux/fidl/fuchsia.feedback/data_register.fidl diff --git a/ci/licenses_golden/licenses_skia b/ci/licenses_golden/licenses_skia index 0a6a344482ce9..f30ab3de5d492 100644 --- a/ci/licenses_golden/licenses_skia +++ b/ci/licenses_golden/licenses_skia @@ -1,4 +1,4 @@ -Signature: cef2ee190e58d4bdbb36c83d85fd943a +Signature: b3e814b93da372c39dbd3a9b96a6ac4e UNUSED LICENSES: @@ -969,6 +969,7 @@ FILE: ../../../third_party/skia/bench/microbench.json FILE: ../../../third_party/skia/bench/skpbench.json FILE: ../../../third_party/skia/build/fuchsia/skqp/skqp.cmx FILE: ../../../third_party/skia/build/fuchsia/skqp/test_manifest.json +FILE: ../../../third_party/skia/demos.skia.org/demos/hello_world/index.html FILE: ../../../third_party/skia/docker/binary-size/Dockerfile FILE: ../../../third_party/skia/docker/cmake-release/Dockerfile FILE: ../../../third_party/skia/docker/skia-build-tools/Dockerfile @@ -2338,6 +2339,7 @@ FILE: ../../../third_party/skia/include/codec/SkAndroidCodec.h FILE: ../../../third_party/skia/include/codec/SkCodec.h FILE: ../../../third_party/skia/include/core/SkEncodedImageFormat.h FILE: ../../../third_party/skia/include/core/SkFilterQuality.h +FILE: ../../../third_party/skia/include/core/SkPathBuilder.h FILE: ../../../third_party/skia/include/core/SkPixmap.h FILE: ../../../third_party/skia/include/core/SkPngChunkReader.h FILE: ../../../third_party/skia/include/core/SkPoint3.h @@ -2425,6 +2427,7 @@ FILE: ../../../third_party/skia/src/core/SkMiniRecorder.h FILE: ../../../third_party/skia/src/core/SkNextID.h FILE: ../../../third_party/skia/src/core/SkOpts.cpp FILE: ../../../third_party/skia/src/core/SkOpts.h +FILE: ../../../third_party/skia/src/core/SkPathBuilder.cpp FILE: ../../../third_party/skia/src/core/SkPathPriv.h FILE: ../../../third_party/skia/src/core/SkPictureCommon.h FILE: ../../../third_party/skia/src/core/SkPictureImageGenerator.cpp @@ -5219,9 +5222,12 @@ TYPE: LicenseType.bsd FILE: ../../../third_party/skia/bench/TessellatePathBench.cpp FILE: ../../../third_party/skia/experimental/skrive/include/SkRive.h FILE: ../../../third_party/skia/experimental/skrive/src/Artboard.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Color.cpp FILE: ../../../third_party/skia/experimental/skrive/src/Component.cpp FILE: ../../../third_party/skia/experimental/skrive/src/Drawable.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Ellipse.cpp FILE: ../../../third_party/skia/experimental/skrive/src/Node.cpp +FILE: ../../../third_party/skia/experimental/skrive/src/Paint.cpp FILE: ../../../third_party/skia/experimental/skrive/src/Shape.cpp FILE: ../../../third_party/skia/experimental/skrive/src/SkRive.cpp FILE: ../../../third_party/skia/experimental/skrive/src/reader/BinaryReader.cpp diff --git a/ci/licenses_golden/licenses_third_party b/ci/licenses_golden/licenses_third_party index fa142dd7b53b8..7f945e1e0185c 100644 --- a/ci/licenses_golden/licenses_third_party +++ b/ci/licenses_golden/licenses_third_party @@ -1,4 +1,4 @@ -Signature: 373a1ce95e3235f255bd904c4e32e9e8 +Signature: a56d6bbdec23954f9cf69a9a3300d035 UNUSED LICENSES: diff --git a/common/settings.h b/common/settings.h index 52abe1994bd60..bf615379637e5 100644 --- a/common/settings.h +++ b/common/settings.h @@ -213,6 +213,15 @@ struct Settings { /// to log a timeline event that tracks the latency of engine startup. std::chrono::microseconds engine_start_timestamp = {}; + /// Whether the application claims that it uses the android embedded view for + /// platform views. + /// + /// A `true` value will result the raster task runner always run on the + /// platform thread. + // TODO(cyanlaz): Remove this when dynamic thread merging is done. + // https://github.com/flutter/flutter/issues/59930 + bool use_embedded_view = false; + std::string ToString() const; }; diff --git a/flow/embedded_views.h b/flow/embedded_views.h index 3493f02be7415..912df00a90ec6 100644 --- a/flow/embedded_views.h +++ b/flow/embedded_views.h @@ -123,7 +123,7 @@ class Mutator { // // For example consider the following stack: [T1, T2, T3], where T1 is the top // of the stack and T3 is the bottom of the stack. Applying this mutators stack -// to a platform view P1 will result in T1(T2(T2(P1))). +// to a platform view P1 will result in T1(T2(T3(P1))). class MutatorsStack { public: MutatorsStack() = default; @@ -287,16 +287,16 @@ class ExternalViewEmbedder { virtual bool SubmitFrame(GrContext* context, std::unique_ptr frame); - // This should only be called after |SubmitFrame|. // This method provides the embedder a way to do additional tasks after - // |SubmitFrame|. After invoking this method, the current task on the - // TaskRunner should end immediately. + // |SubmitFrame|. For example, merge task runners if `should_resubmit_frame` + // is true. // // For example on the iOS embedder, threads are merged in this call. // A new frame on the platform thread starts immediately. If the GPU thread // still has some task running, there could be two frames being rendered // concurrently, which causes undefined behaviors. virtual void EndFrame( + bool should_resubmit_frame, fml::RefPtr raster_thread_merger) {} FML_DISALLOW_COPY_AND_ASSIGN(ExternalViewEmbedder); diff --git a/flutter_frontend_server/lib/server.dart b/flutter_frontend_server/lib/server.dart index 85584d20329aa..e4524a15624e2 100644 --- a/flutter_frontend_server/lib/server.dart +++ b/flutter_frontend_server/lib/server.dart @@ -28,10 +28,12 @@ class _FlutterFrontendCompiler implements frontend.CompilerInterface { _FlutterFrontendCompiler(StringSink output, {bool unsafePackageSerialization, bool useDebuggerModuleNames, + bool emitDebugMetadata, frontend.ProgramTransformer transformer}) : _compiler = frontend.FrontendCompiler(output, transformer: transformer, useDebuggerModuleNames: useDebuggerModuleNames, + emitDebugMetadata: emitDebugMetadata, unsafePackageSerialization: unsafePackageSerialization); @override @@ -171,6 +173,7 @@ Future starter( compiler ??= _FlutterFrontendCompiler(output, transformer: ToStringTransformer(transformer, deleteToStringPackageUris), useDebuggerModuleNames: options['debugger-module-names'] as bool, + emitDebugMetadata: options['experimental-emit-debug-metadata'] as bool, unsafePackageSerialization: options['unsafe-package-serialization'] as bool); diff --git a/lib/web_ui/dev/browser_lock.yaml b/lib/web_ui/dev/browser_lock.yaml index 486d7ec242ec2..57fbf7a6816fe 100644 --- a/lib/web_ui/dev/browser_lock.yaml +++ b/lib/web_ui/dev/browser_lock.yaml @@ -12,5 +12,9 @@ edge: launcher_version: '1.2.0.0' ios-safari: majorVersion: 13 - minorVersion: 5 + minorVersion: 0 device: 'iPhone 11' +## geckodriver is used for testing Firefox Browser. It works with multiple +## Firefox Browser versions. +## See: https://github.com/mozilla/geckodriver/releases +geckodriver: 'v0.26.0' diff --git a/lib/web_ui/dev/driver_manager.dart b/lib/web_ui/dev/driver_manager.dart index 309e3e3513dc9..de15c467c5510 100644 --- a/lib/web_ui/dev/driver_manager.dart +++ b/lib/web_ui/dev/driver_manager.dart @@ -7,7 +7,9 @@ import 'dart:io' as io; import 'package:meta/meta.dart'; import 'package:path/path.dart' as pathlib; import 'package:web_driver_installer/chrome_driver_installer.dart'; +import 'package:web_driver_installer/firefox_driver_installer.dart'; import 'package:web_driver_installer/safari_driver_runner.dart'; +import 'package:yaml/yaml.dart'; import 'chrome_installer.dart'; import 'common.dart'; @@ -16,10 +18,11 @@ import 'utils.dart'; /// [DriverManager] implementation for Chrome. /// -/// This manager can be used for both MacOS and Linux. +/// This manager can be used for both macOS and Linux. class ChromeDriverManager extends DriverManager { ChromeDriverManager(String browser) : super(browser); + @override Future _installDriver() async { if (_browserDriverDir.existsSync()) { _browserDriverDir.deleteSync(recursive: true); @@ -45,6 +48,7 @@ class ChromeDriverManager extends DriverManager { /// Throw an error if driver directory does not exists. /// /// Driver should already exist on LUCI as a CIPD package. + @override Future _verifyDriverForLUCI() { if (!_browserDriverDir.existsSync()) { throw StateError('Failed to locate Chrome driver on LUCI on path:' @@ -53,6 +57,7 @@ class ChromeDriverManager extends DriverManager { return Future.value(); } + @override Future _startDriver(String driverPath) async { await startProcess('./chromedriver/chromedriver', ['--port=4444'], workingDirectory: driverPath); @@ -60,24 +65,85 @@ class ChromeDriverManager extends DriverManager { } } +/// [DriverManager] implementation for Firefox. +/// +/// This manager can be used for both macOS and Linux. +class FirefoxDriverManager extends DriverManager { + FirefoxDriverManager(String browser) : super(browser); + + FirefoxDriverInstaller firefoxDriverInstaller = + FirefoxDriverInstaller(geckoDriverVersion: getLockedGeckoDriverVersion()); + + @override + Future _installDriver() async { + if (_browserDriverDir.existsSync()) { + _browserDriverDir.deleteSync(recursive: true); + } + + _browserDriverDir.createSync(recursive: true); + temporaryDirectories.add(_drivers); + + final io.Directory temp = io.Directory.current; + io.Directory.current = _browserDriverDir; + + try { + await firefoxDriverInstaller.install(alwaysInstall: false); + } finally { + io.Directory.current = temp; + } + } + + /// Throw an error if driver directory does not exist. + /// + /// Driver should already exist on LUCI as a CIPD package. + @override + Future _verifyDriverForLUCI() { + if (!_browserDriverDir.existsSync()) { + throw StateError('Failed to locate Firefox driver on LUCI on path:' + '${_browserDriverDir.path}'); + } + return Future.value(); + } + + @override + Future _startDriver(String driverPath) async { + await startProcess('./firefoxdriver/geckodriver', ['--port=4444'], + workingDirectory: driverPath); + print('INFO: Driver started'); + } + + /// Get the geckodriver version to be used with [FirefoxDriverInstaller]. + /// + /// For different versions of geckodriver. See: + /// https://github.com/mozilla/geckodriver/releases + static String getLockedGeckoDriverVersion() { + final YamlMap browserLock = BrowserLock.instance.configuration; + String geckoDriverReleaseVersion = browserLock['geckodriver'] as String; + return geckoDriverReleaseVersion; + } +} + /// [DriverManager] implementation for Safari. /// -/// This manager is will only be created/used for MacOS. +/// This manager is will only be created/used for macOS. class SafariDriverManager extends DriverManager { SafariDriverManager(String browser) : super(browser); + @override Future _installDriver() { // No-op. // macOS comes with Safari Driver installed. return new Future.value(); } + @override Future _verifyDriverForLUCI() { // No-op. // macOS comes with Safari Driver installed. return Future.value(); } + @override Future _startDriver(String driverPath) async { final SafariDriverRunner safariDriverRunner = SafariDriverRunner(); @@ -137,11 +203,13 @@ abstract class DriverManager { static DriverManager chooseDriver(String browser) { if (browser == 'chrome') { return ChromeDriverManager(browser); + } else if (browser == 'firefox') { + return FirefoxDriverManager(browser); } else if (browser == 'safari' && io.Platform.isMacOS) { return SafariDriverManager(browser); } else { - throw StateError('Integration tests are only supported on Chrome or ' - 'on Safari (running on MacOS)'); + throw StateError('Integration tests are only supported on Firefox, Chrome' + ' and on Safari (running on macOS)'); } } } diff --git a/lib/web_ui/dev/integration_tests_manager.dart b/lib/web_ui/dev/integration_tests_manager.dart index 76a42a1ae76ed..c86209259bcf3 100644 --- a/lib/web_ui/dev/integration_tests_manager.dart +++ b/lib/web_ui/dev/integration_tests_manager.dart @@ -14,8 +14,8 @@ import 'exceptions.dart'; import 'common.dart'; import 'utils.dart'; -const String _unsupportedConfigurationWarning = 'WARNING: integration ' - 'tests are only supported on Chrome or on Safari (running on MacOS)'; +const String _unsupportedConfigurationWarning = 'WARNING: integration tests ' + 'are only supported on Chrome, Firefox and on Safari (running on macOS)'; class IntegrationTestsManager { final String _browser; @@ -285,10 +285,13 @@ class IntegrationTestsManager { /// Validate the given `browser`, `platform` combination is suitable for /// integration tests to run. bool validateIfTestsShouldRun() { - // Chrome tests should run at all Platforms (Linux, MacOS, Windows). + // Chrome tests should run at all Platforms (Linux, macOS, Windows). // They can also run successfully on CI and local. if (_browser == 'chrome') { return true; + } else if (_browser == 'firefox' && + (io.Platform.isLinux || io.Platform.isMacOS)) { + return true; } else if (_browser == 'safari' && io.Platform.isMacOS && !isLuci) { return true; } else { @@ -306,6 +309,8 @@ abstract class IntegrationArguments { factory IntegrationArguments.fromBrowser(String browser) { if (browser == 'chrome') { return ChromeIntegrationArguments(); + } else if (browser == 'firefox') { + return FirefoxIntegrationArguments(); } else if (browser == 'safari' && io.Platform.isMacOS) { return SafariIntegrationArguments(); } else { @@ -346,6 +351,25 @@ class ChromeIntegrationArguments extends IntegrationArguments { } } +/// Arguments to give `flutter drive` to run the integration tests on Firefox. +class FirefoxIntegrationArguments extends IntegrationArguments { + List getTestArguments(String testName, String mode) { + return [ + 'drive', + '--target=test_driver/${testName}', + '-d', + 'web-server', + '--$mode', + '--browser-name=firefox', + '--headless', + '--local-engine=host_debug_unopt', + ]; + } + + String getCommandToRun(String testName, String mode) => + 'flutter ${getTestArguments(testName, mode).join(' ')}'; +} + /// Arguments to give `flutter drive` to run the integration tests on Safari. class SafariIntegrationArguments extends IntegrationArguments { SafariIntegrationArguments(); @@ -397,4 +421,12 @@ const Map> blockedTestsListsMap = >{ 'target_platform_android_e2e.dart', 'image_loading_e2e.dart', ], + 'firefox-linux': [ + 'target_platform_ios_e2e.dart', + 'target_platform_macos_e2e.dart', + ], + 'firefox-macos': [ + 'target_platform_android_e2e.dart', + 'target_platform_ios_e2e.dart', + ], }; diff --git a/lib/web_ui/dev/test_runner.dart b/lib/web_ui/dev/test_runner.dart index ae6edc408458a..d76711398bd8c 100644 --- a/lib/web_ui/dev/test_runner.dart +++ b/lib/web_ui/dev/test_runner.dart @@ -106,7 +106,7 @@ class TestCommand extends Command with ArgUtils { print('Running the unit tests only'); return TestTypesRequested.unit; } else if (boolArg('integration-tests-only')) { - if (!isChrome && !isSafariOnMacOS) { + if (!isChrome && !isSafariOnMacOS && !isFirefox) { throw UnimplementedError( 'Integration tests are only available on Chrome Desktop for now'); } @@ -132,7 +132,7 @@ class TestCommand extends Command with ArgUtils { case TestTypesRequested.all: // TODO(nurhan): https://github.com/flutter/flutter/issues/53322 // TODO(nurhan): Expand browser matrix for felt integration tests. - if (runAllTests && (isChrome || isSafariOnMacOS)) { + if (runAllTests && (isChrome || isSafariOnMacOS || isFirefox)) { bool unitTestResult = await runUnitTests(); bool integrationTestResult = await runIntegrationTests(); if (integrationTestResult != unitTestResult) { @@ -263,6 +263,9 @@ class TestCommand extends Command with ArgUtils { /// Whether [browser] is set to "chrome". bool get isChrome => browser == 'chrome'; + /// Whether [browser] is set to "firefox". + bool get isFirefox => browser == 'firefox'; + /// Whether [browser] is set to "safari". bool get isSafariOnMacOS => browser == 'safari' && io.Platform.isMacOS; diff --git a/lib/web_ui/lib/src/engine/text/font_collection.dart b/lib/web_ui/lib/src/engine/text/font_collection.dart index 95e8d4e684595..e68880fda8efc 100644 --- a/lib/web_ui/lib/src/engine/text/font_collection.dart +++ b/lib/web_ui/lib/src/engine/text/font_collection.dart @@ -98,7 +98,7 @@ class FontCollection { void clear() { _assetFontManager = null; _testFontManager = null; - if (supportsFontLoadingApi) { + if (supportsFontsClearApi) { html.document.fonts.clear(); } } @@ -313,4 +313,5 @@ class _PolyfillFontManager extends FontManager { } } -final bool supportsFontLoadingApi = html.document.fonts != null; +final bool supportsFontLoadingApi = js_util.hasProperty(html.window, 'FontFace'); +final bool supportsFontsClearApi = html.document.fonts != null && js_util.hasProperty(html.document.fonts, 'clear'); diff --git a/lib/web_ui/pubspec.yaml b/lib/web_ui/pubspec.yaml index a95a96bcf42e9..8dabd46ba0a52 100644 --- a/lib/web_ui/pubspec.yaml +++ b/lib/web_ui/pubspec.yaml @@ -31,4 +31,4 @@ dev_dependencies: git: url: git://github.com/flutter/web_installers.git path: packages/web_drivers/ - ref: 41f96bb55d2f064dac3c9fc727ebdf4b0cdf79c4 + ref: 1cea0d79cad1ebc217c4bcbeba1be41470674a49 diff --git a/lib/web_ui/test/canvaskit/path_metrics_test.dart b/lib/web_ui/test/canvaskit/path_metrics_test.dart index 18bab1ced554c..9089a6b57af24 100644 --- a/lib/web_ui/test/canvaskit/path_metrics_test.dart +++ b/lib/web_ui/test/canvaskit/path_metrics_test.dart @@ -9,59 +9,65 @@ import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; void main() { - setUpAll(() async { - await ui.webOnlyInitializePlatform(); - }); + group('Path Metrics', () { + setUpAll(() async { + await ui.webOnlyInitializePlatform(); + }); - test('Using CanvasKit', () { - expect(experimentalUseSkia, true); - }); + test('Using CanvasKit', () { + expect(experimentalUseSkia, true); + }); - test(SkPathMetrics, () { - final ui.Path path = ui.Path(); - expect(path, isA()); - expect(path.computeMetrics().length, 0); + test(SkPathMetrics, () { + final ui.Path path = ui.Path(); + expect(path, isA()); + expect(path.computeMetrics().length, 0); - path.addRect(ui.Rect.fromLTRB(0, 0, 10, 10)); - final ui.PathMetric metric = path.computeMetrics().single; - expect(metric.contourIndex, 0); - expect(metric.extractPath(0, 0.5).computeMetrics().length, 1); + path.addRect(ui.Rect.fromLTRB(0, 0, 10, 10)); + final ui.PathMetric metric = path.computeMetrics().single; + expect(metric.contourIndex, 0); + expect(metric.extractPath(0, 0.5).computeMetrics().length, 1); - final ui.Tangent tangent1 = metric.getTangentForOffset(5); - expect(tangent1.position, ui.Offset(5, 0)); - expect(tangent1.vector, ui.Offset(1, 0)); + final ui.Tangent tangent1 = metric.getTangentForOffset(5); + expect(tangent1.position, ui.Offset(5, 0)); + expect(tangent1.vector, ui.Offset(1, 0)); - final ui.Tangent tangent2 = metric.getTangentForOffset(15); - expect(tangent2.position, ui.Offset(10, 5)); - expect(tangent2.vector, ui.Offset(0, 1)); + final ui.Tangent tangent2 = metric.getTangentForOffset(15); + expect(tangent2.position, ui.Offset(10, 5)); + expect(tangent2.vector, ui.Offset(0, 1)); - expect(metric.isClosed, true); + expect(metric.isClosed, true); - path.addOval(ui.Rect.fromLTRB(10, 10, 100, 100)); - expect(path.computeMetrics().length, 2); + path.addOval(ui.Rect.fromLTRB(10, 10, 100, 100)); + expect(path.computeMetrics().length, 2); - // Path metrics can be iterated over multiple times. - final ui.PathMetrics metrics = path.computeMetrics(); - expect(metrics.toList().length, 2); - expect(metrics.toList().length, 2); - expect(metrics.toList().length, 2); + // Path metrics can be iterated over multiple times. + final ui.PathMetrics metrics = path.computeMetrics(); + expect(metrics.toList().length, 2); + expect(metrics.toList().length, 2); + expect(metrics.toList().length, 2); - // Can simultaneously iterate over multiple metrics from the same path. - final ui.PathMetrics metrics1 = path.computeMetrics(); - final ui.PathMetrics metrics2 = path.computeMetrics(); - final Iterator iter1 = metrics1.iterator; - final Iterator iter2 = metrics2.iterator; - expect(iter1.moveNext(), true); - expect(iter2.moveNext(), true); - expect(iter1.current, isNotNull); - expect(iter2.current, isNotNull); - expect(iter1.moveNext(), true); - expect(iter2.moveNext(), true); - expect(iter1.current, isNotNull); - expect(iter2.current, isNotNull); - expect(iter1.moveNext(), false); - expect(iter2.moveNext(), false); - expect(iter1.current, isNull); - expect(iter2.current, isNull); - }); + // Can simultaneously iterate over multiple metrics from the same path. + final ui.PathMetrics metrics1 = path.computeMetrics(); + final ui.PathMetrics metrics2 = path.computeMetrics(); + final Iterator iter1 = metrics1.iterator; + final Iterator iter2 = metrics2.iterator; + expect(iter1.moveNext(), true); + expect(iter2.moveNext(), true); + expect(iter1.current, isNotNull); + expect(iter2.current, isNotNull); + expect(iter1.moveNext(), true); + expect(iter2.moveNext(), true); + expect(iter1.current, isNotNull); + expect(iter2.current, isNotNull); + expect(iter1.moveNext(), false); + expect(iter2.moveNext(), false); + expect(iter1.current, isNull); + expect(iter2.current, isNull); + }); + }, + // This test failed on iOS Safari. + // TODO: https://github.com/flutter/flutter/issues/60040 + skip: (browserEngine == BrowserEngine.webkit && + operatingSystem == OperatingSystem.iOs)); } diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index 3f8ed22b0968c..5f6ac793c38ff 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -34,6 +34,11 @@ void _testEach( } } +/// Some methods in this class are skipped for iOS-Safari. +/// TODO: https://github.com/flutter/flutter/issues/60033 +bool get isIosSafari => (browserEngine == BrowserEngine.webkit && + operatingSystem == OperatingSystem.iOs); + void main() { html.Element glassPane = domRenderer.glassPaneElement; @@ -45,13 +50,15 @@ void main() { }); test('_PointerEventContext generates expected events', () { - if (!_PointerEventContext().isSupported) + if (!_PointerEventContext().isSupported) { return; + } html.PointerEvent expectCorrectType(html.Event e) { expect(e.runtimeType, equals(html.PointerEvent)); return e; } + List expectCorrectTypes(List events) { return events.map(expectCorrectType).toList(); } @@ -68,7 +75,8 @@ void main() { expect(event.client.x, equals(100)); expect(event.client.y, equals(101)); - event = expectCorrectType(context.mouseDown(clientX: 110, clientY: 111, button: 2, buttons: 2)); + event = expectCorrectType( + context.mouseDown(clientX: 110, clientY: 111, button: 2, buttons: 2)); expect(event.type, equals('pointerdown')); expect(event.pointerId, equals(1)); expect(event.button, equals(2)); @@ -102,7 +110,8 @@ void main() { expect(event.client.x, equals(200)); expect(event.client.y, equals(201)); - event = expectCorrectType(context.mouseMove(clientX: 210, clientY: 211, button: _kNoButtonChange, buttons: 6)); + event = expectCorrectType(context.mouseMove( + clientX: 210, clientY: 211, button: _kNoButtonChange, buttons: 6)); expect(event.type, equals('pointermove')); expect(event.pointerId, equals(1)); expect(event.button, equals(-1)); @@ -110,7 +119,8 @@ void main() { expect(event.client.x, equals(210)); expect(event.client.y, equals(211)); - event = expectCorrectType(context.mouseMove(clientX: 212, clientY: 213, button: 2, buttons: 6)); + event = expectCorrectType( + context.mouseMove(clientX: 212, clientY: 213, button: 2, buttons: 6)); expect(event.type, equals('pointermove')); expect(event.pointerId, equals(1)); expect(event.button, equals(2)); @@ -118,7 +128,8 @@ void main() { expect(event.client.x, equals(212)); expect(event.client.y, equals(213)); - event = expectCorrectType(context.mouseMove(clientX: 214, clientY: 215, button: 2, buttons: 1)); + event = expectCorrectType( + context.mouseMove(clientX: 214, clientY: 215, button: 2, buttons: 1)); expect(event.type, equals('pointermove')); expect(event.pointerId, equals(1)); expect(event.button, equals(2)); @@ -152,7 +163,8 @@ void main() { expect(event.client.x, equals(300)); expect(event.client.y, equals(301)); - event = expectCorrectType(context.mouseUp(clientX: 310, clientY: 311, button: 2)); + event = expectCorrectType( + context.mouseUp(clientX: 310, clientY: 311, button: 2)); expect(event.type, equals('pointerup')); expect(event.pointerId, equals(1)); expect(event.button, equals(2)); @@ -206,13 +218,15 @@ void main() { }); test('_TouchEventContext generates expected events', () { - if (!_TouchEventContext().isSupported) + if (!_TouchEventContext().isSupported) { return; + } html.TouchEvent expectCorrectType(html.Event e) { expect(e.runtimeType, equals(html.TouchEvent)); return e; } + List expectCorrectTypes(List events) { return events.map(expectCorrectType).toList(); } @@ -300,8 +314,9 @@ void main() { }); test('_MouseEventContext generates expected events', () { - if (!_MouseEventContext().isSupported) + if (!_MouseEventContext().isSupported) { return; + } html.MouseEvent expectCorrectType(html.Event e) { expect(e.runtimeType, equals(html.MouseEvent)); @@ -318,7 +333,8 @@ void main() { expect(event.client.x, equals(100)); expect(event.client.y, equals(101)); - event = expectCorrectType(context.mouseDown(clientX: 110, clientY: 111, button: 2, buttons: 2)); + event = expectCorrectType( + context.mouseDown(clientX: 110, clientY: 111, button: 2, buttons: 2)); expect(event.type, equals('mousedown')); expect(event.button, equals(2)); expect(event.buttons, equals(2)); @@ -332,21 +348,24 @@ void main() { expect(event.client.x, equals(200)); expect(event.client.y, equals(201)); - event = expectCorrectType(context.mouseMove(clientX: 210, clientY: 211, button: _kNoButtonChange, buttons: 6)); + event = expectCorrectType(context.mouseMove( + clientX: 210, clientY: 211, button: _kNoButtonChange, buttons: 6)); expect(event.type, equals('mousemove')); expect(event.button, equals(0)); expect(event.buttons, equals(6)); expect(event.client.x, equals(210)); expect(event.client.y, equals(211)); - event = expectCorrectType(context.mouseMove(clientX: 212, clientY: 213, button: 2, buttons: 6)); + event = expectCorrectType( + context.mouseMove(clientX: 212, clientY: 213, button: 2, buttons: 6)); expect(event.type, equals('mousedown')); expect(event.button, equals(2)); expect(event.buttons, equals(6)); expect(event.client.x, equals(212)); expect(event.client.y, equals(213)); - event = expectCorrectType(context.mouseMove(clientX: 214, clientY: 215, button: 2, buttons: 1)); + event = expectCorrectType( + context.mouseMove(clientX: 214, clientY: 215, button: 2, buttons: 1)); expect(event.type, equals('mouseup')); expect(event.button, equals(2)); expect(event.buttons, equals(1)); @@ -360,7 +379,8 @@ void main() { expect(event.client.x, equals(300)); expect(event.client.y, equals(301)); - event = expectCorrectType(context.mouseUp(clientX: 310, clientY: 311, button: 2)); + event = expectCorrectType( + context.mouseUp(clientX: 310, clientY: 311, button: 2)); expect(event.type, equals('mouseup')); expect(event.button, equals(2)); expect(event.buttons, equals(0)); @@ -378,7 +398,11 @@ void main() { // ALL ADAPTERS _testEach<_BasicEventContext>( - [_PointerEventContext(), _MouseEventContext(), _TouchEventContext()], + [ + _PointerEventContext(), + _MouseEventContext(), + _TouchEventContext(), + ], 'can receive pointer events on the glass pane', (_BasicEventContext context) { PointerBinding.instance.debugOverrideDetector(context); @@ -395,7 +419,11 @@ void main() { ); _testEach<_BasicEventContext>( - [_PointerEventContext(), _MouseEventContext(), _TouchEventContext()], + [ + _PointerEventContext(), + _MouseEventContext(), + _TouchEventContext(), + ], 'does create an add event if got a pointerdown', (_BasicEventContext context) { PointerBinding.instance.debugOverrideDetector(context); @@ -415,7 +443,10 @@ void main() { ); _testEach<_ButtonedEventMixin>( - [_PointerEventContext(), _MouseEventContext()], + [ + if (!isIosSafari) _PointerEventContext(), + if (!isIosSafari) _MouseEventContext(), + ], 'correctly detects events on the semantics placeholder', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -486,7 +517,10 @@ void main() { // BUTTONED ADAPTERS _testEach<_ButtonedEventMixin>( - [_MouseEventContext(), _PointerEventContext()], + [ + _PointerEventContext(), + _MouseEventContext(), + ], 'creates an add event if the first pointer activity is a hover', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -507,7 +541,10 @@ void main() { ); _testEach<_ButtonedEventMixin>( - [_PointerEventContext(), _MouseEventContext()], + [ + _PointerEventContext(), + _MouseEventContext(), + ], 'sends a pointermove event instead of the second pointerdown in a row', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -541,7 +578,10 @@ void main() { ); _testEach<_ButtonedEventMixin>( - [_PointerEventContext(), _MouseEventContext()], + [ + if (!isIosSafari) _PointerEventContext(), + if (!isIosSafari) _MouseEventContext(), + ], 'does synthesize add or hover or move for scroll', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -550,7 +590,8 @@ void main() { packets.add(packet); }; - glassPane.dispatchEvent(html.WheelEvent('wheel', + glassPane.dispatchEvent(html.WheelEvent( + 'wheel', button: 1, clientX: 10, clientY: 10, @@ -558,7 +599,8 @@ void main() { deltaY: 10, )); - glassPane.dispatchEvent(html.WheelEvent('wheel', + glassPane.dispatchEvent(html.WheelEvent( + 'wheel', button: 1, clientX: 20, clientY: 50, @@ -573,7 +615,8 @@ void main() { clientY: 50.0, )); - glassPane.dispatchEvent(html.WheelEvent('wheel', + glassPane.dispatchEvent(html.WheelEvent( + 'wheel', button: 1, clientX: 30, clientY: 60, @@ -594,7 +637,8 @@ void main() { expect(packets[0].data[0].physicalDeltaY, equals(0.0)); expect(packets[0].data[1].change, equals(ui.PointerChange.hover)); - expect(packets[0].data[1].signalKind, equals(ui.PointerSignalKind.scroll)); + expect( + packets[0].data[1].signalKind, equals(ui.PointerSignalKind.scroll)); expect(packets[0].data[1].pointerIdentifier, equals(0)); expect(packets[0].data[1].synthesized, equals(false)); expect(packets[0].data[1].physicalX, equals(10.0)); @@ -613,7 +657,8 @@ void main() { expect(packets[1].data[0].physicalDeltaY, equals(40.0)); expect(packets[1].data[1].change, equals(ui.PointerChange.hover)); - expect(packets[1].data[1].signalKind, equals(ui.PointerSignalKind.scroll)); + expect( + packets[1].data[1].signalKind, equals(ui.PointerSignalKind.scroll)); expect(packets[1].data[1].pointerIdentifier, equals(0)); expect(packets[1].data[1].synthesized, equals(false)); expect(packets[1].data[1].physicalX, equals(20.0)); @@ -643,7 +688,8 @@ void main() { expect(packets[3].data[0].physicalDeltaY, equals(10.0)); expect(packets[3].data[1].change, equals(ui.PointerChange.hover)); - expect(packets[3].data[1].signalKind, equals(ui.PointerSignalKind.scroll)); + expect( + packets[3].data[1].signalKind, equals(ui.PointerSignalKind.scroll)); expect(packets[3].data[1].pointerIdentifier, equals(1)); expect(packets[3].data[1].synthesized, equals(false)); expect(packets[3].data[1].physicalX, equals(30.0)); @@ -654,7 +700,10 @@ void main() { ); _testEach<_ButtonedEventMixin>( - [_PointerEventContext(), _MouseEventContext()], + [ + if (!isIosSafari) _PointerEventContext(), + if (!isIosSafari) _MouseEventContext() + ], 'does calculate delta and pointer identifier correctly', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -779,7 +828,10 @@ void main() { ); _testEach<_ButtonedEventMixin>( - [_PointerEventContext(), _MouseEventContext()], + [ + if (!isIosSafari) _PointerEventContext(), + if (!isIosSafari) _MouseEventContext(), + ], 'correctly converts buttons of down, move and up events', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -946,7 +998,10 @@ void main() { ); _testEach<_ButtonedEventMixin>( - [_PointerEventContext(), _MouseEventContext()], + [ + _PointerEventContext(), + _MouseEventContext(), + ], 'correctly handles button changes during a down sequence', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -1008,7 +1063,10 @@ void main() { ); _testEach<_ButtonedEventMixin>( - [_PointerEventContext(), _MouseEventContext()], + [ + if (!isIosSafari) _PointerEventContext(), + if (!isIosSafari) _MouseEventContext(), + ], 'synthesizes a pointerup event when pointermove comes before the up', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -1056,7 +1114,6 @@ void main() { expect(packets[0].data[0].buttons, equals(2)); packets.clear(); - glassPane.dispatchEvent(context.mouseMove( button: _kNoButtonChange, buttons: 2, @@ -1072,7 +1129,6 @@ void main() { expect(packets[0].data[0].buttons, equals(2)); packets.clear(); - glassPane.dispatchEvent(context.mouseUp( button: 2, clientX: 20.0, @@ -1090,7 +1146,10 @@ void main() { ); _testEach<_ButtonedEventMixin>( - [_PointerEventContext(), _MouseEventContext()], + [ + _PointerEventContext(), + _MouseEventContext(), + ], 'correctly handles uncontinuous button changes during a down sequence', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -1160,7 +1219,10 @@ void main() { ); _testEach<_ButtonedEventMixin>( - [_PointerEventContext(), _MouseEventContext()], + [ + _PointerEventContext(), + _MouseEventContext(), + ], 'handles RMB click when the browser sends it as a move', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -1193,7 +1255,10 @@ void main() { ); _testEach<_ButtonedEventMixin>( - [_PointerEventContext(), _MouseEventContext()], + [ + _PointerEventContext(), + _MouseEventContext(), + ], 'correctly handles hover after RMB click', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -1241,7 +1306,10 @@ void main() { ); _testEach<_ButtonedEventMixin>( - [_PointerEventContext(), _MouseEventContext()], + [ + _PointerEventContext(), + _MouseEventContext(), + ], 'correctly handles LMB click after RMB click', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -1307,7 +1375,10 @@ void main() { ); _testEach<_ButtonedEventMixin>( - [_PointerEventContext(), _MouseEventContext()], + [ + _PointerEventContext(), + _MouseEventContext(), + ], 'correctly handles two consecutive RMB clicks with no up in between', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -1369,7 +1440,10 @@ void main() { ); _testEach<_ButtonedEventMixin>( - [_PointerEventContext(), _MouseEventContext()], + [ + _PointerEventContext(), + _MouseEventContext(), + ], 'correctly handles two consecutive RMB clicks with up in between', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -1449,7 +1523,10 @@ void main() { ); _testEach<_ButtonedEventMixin>( - [_PointerEventContext(), _MouseEventContext()], + [ + _PointerEventContext(), + _MouseEventContext(), + ], 'correctly handles two consecutive RMB clicks in two different locations', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -1516,7 +1593,10 @@ void main() { ); _testEach<_ButtonedEventMixin>( - [_PointerEventContext(), _MouseEventContext()], + [ + if (!isIosSafari) _PointerEventContext(), + if (!isIosSafari) _MouseEventContext(), + ], 'correctly detects up event outside of glasspane', (_ButtonedEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -1579,7 +1659,10 @@ void main() { // MULTIPOINTER ADAPTERS _testEach<_MultiPointerEventMixin>( - [_PointerEventContext(), _TouchEventContext()], + [ + if (!isIosSafari) _PointerEventContext(), + if (!isIosSafari) _TouchEventContext(), + ], 'treats each pointer separately', (_MultiPointerEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -1767,7 +1850,10 @@ void main() { ); _testEach<_MultiPointerEventMixin>( - [_PointerEventContext(), _TouchEventContext()], + [ + if (!isIosSafari) _PointerEventContext(), + if (!isIosSafari) _TouchEventContext(), + ], 'correctly parses cancel event', (_MultiPointerEventMixin context) { PointerBinding.instance.debugOverrideDetector(context); @@ -1811,7 +1897,9 @@ void main() { // POINTER ADAPTER _testEach<_PointerEventContext>( - [_PointerEventContext()], + [ + if (!isIosSafari) _PointerEventContext(), + ], 'does not synthesize pointer up if from different device', (_PointerEventContext context) { PointerBinding.instance.debugOverrideDetector(context); @@ -1851,7 +1939,9 @@ void main() { // TOUCH ADAPTER _testEach( - [_TouchEventContext()], + [ + if (!isIosSafari) _TouchEventContext(), + ], 'does calculate delta and pointer identifier correctly', (_TouchEventContext context) { // Mouse and Pointer are in another test since these tests can involve hovering @@ -1955,14 +2045,12 @@ abstract class _BasicEventContext implements PointerSupportDetector { // * For touch, a touch down html.Event primaryDown({double clientX, double clientY}); - // Generate an event that is: // // * For mouse, a drag with LMB down // * For touch, a touch drag html.Event primaryMove({double clientX, double clientY}); - // Generate an event that is: // // * For mouse, release LMB @@ -1972,13 +2060,15 @@ abstract class _BasicEventContext implements PointerSupportDetector { mixin _ButtonedEventMixin on _BasicEventContext { // Generate an event that is a mouse down with the specific buttons. - html.Event mouseDown({double clientX, double clientY, int button, int buttons}); + html.Event mouseDown( + {double clientX, double clientY, int button, int buttons}); // Generate an event that is a mouse drag with the specific buttons, or button // changes during the drag. // // If there is no button change, assign `button` with _kNoButtonChange. - html.Event mouseMove({double clientX, double clientY, int button, int buttons}); + html.Event mouseMove( + {double clientX, double clientY, int button, int buttons}); // Generate an event that releases all mouse buttons. html.Event mouseUp({double clientX, double clientY, int button}); @@ -2002,7 +2092,6 @@ mixin _ButtonedEventMixin on _BasicEventContext { ); } - @override html.Event primaryMove({double clientX, double clientY}) { return mouseMove( @@ -2073,7 +2162,9 @@ mixin _MultiPointerEventMixin on _BasicEventContext { // A test context for `_TouchAdapter`, including its name, PointerSupportDetector // to override, and how to generate events. -class _TouchEventContext extends _BasicEventContext with _MultiPointerEventMixin implements PointerSupportDetector { +class _TouchEventContext extends _BasicEventContext + with _MultiPointerEventMixin + implements PointerSupportDetector { _TouchEventContext() { _target = html.document.createElement('div'); } @@ -2108,16 +2199,20 @@ class _TouchEventContext extends _BasicEventContext with _MultiPointerEventMixin }); } - html.TouchEvent _createTouchEvent(String eventType, List<_TouchDetails> touches) { - return html.TouchEvent(eventType, { - 'changedTouches': touches.map( - (_TouchDetails details) => - _createTouch( - identifier: details.pointer, - clientX: details.clientX, - clientY: details.clientY, - ), - ).toList(), + html.TouchEvent _createTouchEvent( + String eventType, List<_TouchDetails> touches) { + return html.TouchEvent( + eventType, + { + 'changedTouches': touches + .map( + (_TouchDetails details) => _createTouch( + identifier: details.pointer, + clientX: details.clientX, + clientY: details.clientY, + ), + ) + .toList(), }, ); } @@ -2147,7 +2242,9 @@ class _TouchEventContext extends _BasicEventContext with _MultiPointerEventMixin // to override, and how to generate events. // // For the difference between MouseEvent and PointerEvent, see _MouseAdapter. -class _MouseEventContext extends _BasicEventContext with _ButtonedEventMixin implements PointerSupportDetector { +class _MouseEventContext extends _BasicEventContext + with _ButtonedEventMixin + implements PointerSupportDetector { @override String get name => 'MouseAdapter'; @@ -2164,7 +2261,8 @@ class _MouseEventContext extends _BasicEventContext with _ButtonedEventMixin imp bool get hasMouseEvents => true; @override - html.Event mouseDown({double clientX, double clientY, int button, int buttons}) { + html.Event mouseDown( + {double clientX, double clientY, int button, int buttons}) { return _createMouseEvent( 'mousedown', buttons: buttons, @@ -2175,12 +2273,14 @@ class _MouseEventContext extends _BasicEventContext with _ButtonedEventMixin imp } @override - html.Event mouseMove({double clientX, double clientY, int button, int buttons}) { + html.Event mouseMove( + {double clientX, double clientY, int button, int buttons}) { final bool hasButtonChange = button != _kNoButtonChange; - final bool changeIsButtonDown = hasButtonChange && (buttons & convertButtonToButtons(button)) != 0; - final String adjustedType = !hasButtonChange ? 'mousemove' : - changeIsButtonDown ? 'mousedown' : - 'mouseup'; + final bool changeIsButtonDown = + hasButtonChange && (buttons & convertButtonToButtons(button)) != 0; + final String adjustedType = !hasButtonChange + ? 'mousemove' + : changeIsButtonDown ? 'mousedown' : 'mouseup'; final int adjustedButton = hasButtonChange ? button : 0; return _createMouseEvent( adjustedType, @@ -2209,7 +2309,8 @@ class _MouseEventContext extends _BasicEventContext with _ButtonedEventMixin imp double clientX, double clientY, }) { - final Function jsMouseEvent = js_util.getProperty(html.window, 'MouseEvent'); + final Function jsMouseEvent = + js_util.getProperty(html.window, 'MouseEvent'); final List eventArgs = [ type, { @@ -2227,7 +2328,9 @@ class _MouseEventContext extends _BasicEventContext with _ButtonedEventMixin imp // to override, and how to generate events. // // For the difference between MouseEvent and PointerEvent, see _MouseAdapter. -class _PointerEventContext extends _BasicEventContext with _ButtonedEventMixin implements PointerSupportDetector, _MultiPointerEventMixin { +class _PointerEventContext extends _BasicEventContext + with _ButtonedEventMixin + implements PointerSupportDetector, _MultiPointerEventMixin { @override String get name => 'PointerAdapter'; @@ -2245,18 +2348,21 @@ class _PointerEventContext extends _BasicEventContext with _ButtonedEventMixin i @override List multiTouchDown(List<_TouchDetails> touches) { - return touches.map((_TouchDetails details) => _downWithFullDetails( - pointer: details.pointer, - buttons: 1, - button: 0, - clientX: details.clientX, - clientY: details.clientY, - pointerType: 'touch', - )).toList(); + return touches + .map((_TouchDetails details) => _downWithFullDetails( + pointer: details.pointer, + buttons: 1, + button: 0, + clientX: details.clientX, + clientY: details.clientY, + pointerType: 'touch', + )) + .toList(); } @override - html.Event mouseDown({double clientX, double clientY, int button, int buttons}) { + html.Event mouseDown( + {double clientX, double clientY, int button, int buttons}) { return _downWithFullDetails( pointer: 1, buttons: buttons, @@ -2267,7 +2373,13 @@ class _PointerEventContext extends _BasicEventContext with _ButtonedEventMixin i ); } - html.Event _downWithFullDetails({double clientX, double clientY, int button, int buttons, int pointer, String pointerType}) { + html.Event _downWithFullDetails( + {double clientX, + double clientY, + int button, + int buttons, + int pointer, + String pointerType}) { return html.PointerEvent('pointerdown', { 'pointerId': pointer, 'button': button, @@ -2280,18 +2392,21 @@ class _PointerEventContext extends _BasicEventContext with _ButtonedEventMixin i @override List multiTouchMove(List<_TouchDetails> touches) { - return touches.map((_TouchDetails details) => _moveWithFullDetails( - pointer: details.pointer, - buttons: 1, - button: _kNoButtonChange, - clientX: details.clientX, - clientY: details.clientY, - pointerType: 'touch', - )).toList(); + return touches + .map((_TouchDetails details) => _moveWithFullDetails( + pointer: details.pointer, + buttons: 1, + button: _kNoButtonChange, + clientX: details.clientX, + clientY: details.clientY, + pointerType: 'touch', + )) + .toList(); } @override - html.Event mouseMove({double clientX, double clientY, int button, int buttons}) { + html.Event mouseMove( + {double clientX, double clientY, int button, int buttons}) { return _moveWithFullDetails( pointer: 1, buttons: buttons, @@ -2302,7 +2417,13 @@ class _PointerEventContext extends _BasicEventContext with _ButtonedEventMixin i ); } - html.Event _moveWithFullDetails({double clientX, double clientY, int button, int buttons, int pointer, String pointerType}) { + html.Event _moveWithFullDetails( + {double clientX, + double clientY, + int button, + int buttons, + int pointer, + String pointerType}) { return html.PointerEvent('pointermove', { 'pointerId': pointer, 'button': button, @@ -2315,13 +2436,15 @@ class _PointerEventContext extends _BasicEventContext with _ButtonedEventMixin i @override List multiTouchUp(List<_TouchDetails> touches) { - return touches.map((_TouchDetails details) => _upWithFullDetails( - pointer: details.pointer, - button: 0, - clientX: details.clientX, - clientY: details.clientY, - pointerType: 'touch', - )).toList(); + return touches + .map((_TouchDetails details) => _upWithFullDetails( + pointer: details.pointer, + button: 0, + clientX: details.clientX, + clientY: details.clientY, + pointerType: 'touch', + )) + .toList(); } @override @@ -2335,7 +2458,12 @@ class _PointerEventContext extends _BasicEventContext with _ButtonedEventMixin i ); } - html.Event _upWithFullDetails({double clientX, double clientY, int button, int pointer, String pointerType}) { + html.Event _upWithFullDetails( + {double clientX, + double clientY, + int button, + int pointer, + String pointerType}) { return html.PointerEvent('pointerup', { 'pointerId': pointer, 'button': button, @@ -2348,13 +2476,16 @@ class _PointerEventContext extends _BasicEventContext with _ButtonedEventMixin i @override List multiTouchCancel(List<_TouchDetails> touches) { - return touches.map((_TouchDetails details) => html.PointerEvent('pointercancel', { - 'pointerId': details.pointer, - 'button': 0, - 'buttons': 0, - 'clientX': 0, - 'clientY': 0, - 'pointerType': 'touch', - })).toList(); + return touches + .map((_TouchDetails details) => + html.PointerEvent('pointercancel', { + 'pointerId': details.pointer, + 'button': 0, + 'buttons': 0, + 'clientX': 0, + 'clientY': 0, + 'pointerType': 'touch', + })) + .toList(); } } diff --git a/lib/web_ui/test/engine/surface/surface_test.dart b/lib/web_ui/test/engine/surface/surface_test.dart index 957cacb92f932..9da82f026821e 100644 --- a/lib/web_ui/test/engine/surface/surface_test.dart +++ b/lib/web_ui/test/engine/surface/surface_test.dart @@ -194,7 +194,11 @@ void main() { expect(elementC.parent, elementA); expect(elementB.parent, null); - }); + }, + // This method failed on iOS Safari. + // TODO: https://github.com/flutter/flutter/issues/60036 + skip: (browserEngine == BrowserEngine.webkit && + operatingSystem == OperatingSystem.iOs)); test('is retained', () { final SceneBuilder builder1 = SceneBuilder(); diff --git a/shell/common/rasterizer.cc b/shell/common/rasterizer.cc index afcce2960a6a6..ca69a7b2abdf5 100644 --- a/shell/common/rasterizer.cc +++ b/shell/common/rasterizer.cc @@ -151,12 +151,10 @@ void Rasterizer::Draw(fml::RefPtr> pipeline) { // Merging the thread as we know the next `Draw` should be run on the platform // thread. - if (raster_status == RasterStatus::kResubmit) { - auto* external_view_embedder = surface_->GetExternalViewEmbedder(); - // We know only the `external_view_embedder` can - // causes|RasterStatus::kResubmit|. Check to make sure. - FML_DCHECK(external_view_embedder != nullptr); - external_view_embedder->EndFrame(raster_thread_merger_); + if (surface_ != nullptr && surface_->GetExternalViewEmbedder() != nullptr) { + auto should_resubmit_frame = raster_status == RasterStatus::kResubmit; + surface_->GetExternalViewEmbedder()->EndFrame(should_resubmit_frame, + raster_thread_merger_); } // Consume as many pipeline items as possible. But yield the event loop diff --git a/shell/common/shell.cc b/shell/common/shell.cc index 4acf8af36799c..976d1befb37b5 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -431,6 +431,9 @@ Shell::~Shell() { } void Shell::NotifyLowMemoryWarning() const { + auto trace_id = fml::tracing::TraceNonce(); + TRACE_EVENT_ASYNC_BEGIN0("flutter", "Shell::NotifyLowMemoryWarning", + trace_id); // This does not require a current isolate but does require a running VM. // Since a valid shell will not be returned to the embedder without a valid // DartVMRef, we can be certain that this is a safe spot to assume a VM is @@ -438,10 +441,12 @@ void Shell::NotifyLowMemoryWarning() const { ::Dart_NotifyLowMemory(); task_runners_.GetRasterTaskRunner()->PostTask( - [rasterizer = rasterizer_->GetWeakPtr()]() { + [rasterizer = rasterizer_->GetWeakPtr(), trace_id = trace_id]() { if (rasterizer) { rasterizer->NotifyLowMemoryWarning(); } + TRACE_EVENT_ASYNC_END0("flutter", "Shell::NotifyLowMemoryWarning", + trace_id); }); // The IO Manager uses resource cache limits of 0, so it is not necessary // to purge them. diff --git a/shell/common/shell_test_external_view_embedder.cc b/shell/common/shell_test_external_view_embedder.cc index 306c1b0016b44..ba7aa22cb22f9 100644 --- a/shell/common/shell_test_external_view_embedder.cc +++ b/shell/common/shell_test_external_view_embedder.cc @@ -43,8 +43,9 @@ bool ShellTestExternalViewEmbedder::SubmitFrame( // |ExternalViewEmbedder| void ShellTestExternalViewEmbedder::EndFrame( + bool should_resubmit_frame, fml::RefPtr raster_thread_merger) { - end_frame_call_back_(); + end_frame_call_back_(should_resubmit_frame); } // |ExternalViewEmbedder| diff --git a/shell/common/shell_test_external_view_embedder.h b/shell/common/shell_test_external_view_embedder.h index da96503a9bb46..26072331eae39 100644 --- a/shell/common/shell_test_external_view_embedder.h +++ b/shell/common/shell_test_external_view_embedder.h @@ -15,7 +15,7 @@ namespace flutter { /// class ShellTestExternalViewEmbedder final : public ExternalViewEmbedder { public: - using EndFrameCallBack = std::function; + using EndFrameCallBack = std::function; ShellTestExternalViewEmbedder(const EndFrameCallBack& end_frame_call_back, PostPrerollResult post_preroll_result) @@ -56,6 +56,7 @@ class ShellTestExternalViewEmbedder final : public ExternalViewEmbedder { // |ExternalViewEmbedder| void EndFrame( + bool should_resubmit_frame, fml::RefPtr raster_thread_merger) override; // |ExternalViewEmbedder| diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index 9b3da6833f60d..b9ba1e09dd1b5 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -474,7 +474,8 @@ TEST_F(ShellTest, auto settings = CreateSettingsForFixture(); fml::AutoResetWaitableEvent endFrameLatch; bool end_frame_called = false; - auto end_frame_callback = [&] { + auto end_frame_callback = [&](bool should_resubmit_frame) { + ASSERT_TRUE(should_resubmit_frame); end_frame_called = true; endFrameLatch.Signal(); }; diff --git a/shell/common/switches.cc b/shell/common/switches.cc index e3f6e985d4d74..34c4c4ee56f46 100644 --- a/shell/common/switches.cc +++ b/shell/common/switches.cc @@ -227,6 +227,9 @@ Settings SettingsFromCommandLine(const fml::CommandLine& command_line) { : "127.0.0.1"; } + settings.use_embedded_view = + command_line.HasOption(FlagForSwitch(Switch::UseEmbeddedView)); + // Set Observatory Port if (command_line.HasOption(FlagForSwitch(Switch::DeviceObservatoryPort))) { if (!GetSwitchValue(command_line, Switch::DeviceObservatoryPort, diff --git a/shell/common/switches.h b/shell/common/switches.h index 3bef7d645dde9..8506bc7e72496 100644 --- a/shell/common/switches.h +++ b/shell/common/switches.h @@ -189,6 +189,15 @@ DEF_SWITCH( "Uses separate threads for the platform, UI, GPU and IO task runners. " "By default, a single thread is used for all task runners. Only available " "in the flutter_tester.") +// TODO(cyanlaz): Remove this when dynamic thread merging is done. +// https://github.com/flutter/flutter/issues/59930 +DEF_SWITCH(UseEmbeddedView, + "use-embedded-view", + "Whether an android application uses embedded views." + "This is a temporary flag to make the raster task runner runs on " + "the platform thread." + "This flag should be removed once the dynamic thread merging is " + "enabled on android.") DEF_SWITCHES_END void PrintUsage(const std::string& executable_name); diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 9680f061afdbc..f83390662a6a9 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -135,6 +135,7 @@ android_java_sources = [ "io/flutter/embedding/android/FlutterEngineProvider.java", "io/flutter/embedding/android/FlutterFragment.java", "io/flutter/embedding/android/FlutterFragmentActivity.java", + "io/flutter/embedding/android/FlutterImageView.java", "io/flutter/embedding/android/FlutterSplashView.java", "io/flutter/embedding/android/FlutterSurfaceView.java", "io/flutter/embedding/android/FlutterTextureView.java", diff --git a/shell/platform/android/android_context_gl.cc b/shell/platform/android/android_context_gl.cc index e72b5be5e7898..91e2628e1fc1b 100644 --- a/shell/platform/android/android_context_gl.cc +++ b/shell/platform/android/android_context_gl.cc @@ -59,7 +59,7 @@ static void LogLastEGLError() { FML_LOG(ERROR) << "Unknown EGL Error"; } -static EGLResult CreateContext(EGLDisplay display, +static EGLResult CreateContext(EGLDisplay display, EGLConfig config, EGLContext share = EGL_NO_CONTEXT) { EGLint attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; diff --git a/shell/platform/android/android_shell_holder.cc b/shell/platform/android/android_shell_holder.cc index 62f8acf6fc8fd..c9667dcb7edf3 100644 --- a/shell/platform/android/android_shell_holder.cc +++ b/shell/platform/android/android_shell_holder.cc @@ -28,6 +28,8 @@ static WindowData GetDefaultWindowData() { return window_data; } +bool AndroidShellHolder::use_embedded_view; + AndroidShellHolder::AndroidShellHolder( flutter::Settings settings, std::shared_ptr jni_facade, @@ -101,20 +103,47 @@ AndroidShellHolder::AndroidShellHolder( ui_runner = thread_host_.ui_thread->GetTaskRunner(); io_runner = thread_host_.io_thread->GetTaskRunner(); } - flutter::TaskRunners task_runners(thread_label, // label - platform_runner, // platform - gpu_runner, // raster - ui_runner, // ui - io_runner // io - ); - - shell_ = - Shell::Create(task_runners, // task runners - GetDefaultWindowData(), // window data - settings_, // settings - on_create_platform_view, // platform view create callback - on_create_rasterizer // rasterizer create callback - ); + if (settings.use_embedded_view) { + use_embedded_view = true; + // Embedded views requires the gpu and the platform views to be the same. + // The plan is to eventually dynamically merge the threads when there's a + // platform view in the layer tree. + // For now we use a fixed thread configuration with the same thread used as + // the gpu and platform task runner. + // TODO(amirh/chinmaygarde): remove this, and dynamically change the thread + // configuration. https://github.com/flutter/flutter/issues/23975 + // https://github.com/flutter/flutter/issues/59930 + flutter::TaskRunners task_runners(thread_label, // label + platform_runner, // platform + platform_runner, // raster + ui_runner, // ui + io_runner // io + ); + + shell_ = + Shell::Create(task_runners, // task runners + GetDefaultWindowData(), // window data + settings_, // settings + on_create_platform_view, // platform view create callback + on_create_rasterizer // rasterizer create callback + ); + } else { + use_embedded_view = false; + flutter::TaskRunners task_runners(thread_label, // label + platform_runner, // platform + gpu_runner, // raster + ui_runner, // ui + io_runner // io + ); + + shell_ = + Shell::Create(task_runners, // task runners + GetDefaultWindowData(), // window data + settings_, // settings + on_create_platform_view, // platform view create callback + on_create_rasterizer // rasterizer create callback + ); + } platform_view_ = weak_platform_view; FML_DCHECK(platform_view_); @@ -122,7 +151,7 @@ AndroidShellHolder::AndroidShellHolder( is_valid_ = shell_ != nullptr; if (is_valid_) { - task_runners.GetRasterTaskRunner()->PostTask([]() { + shell_->GetTaskRunners().GetRasterTaskRunner()->PostTask([]() { // Android describes -8 as "most important display threads, for // compositing the screen and retrieving input events". Conservatively // set the raster thread to slightly lower priority than it. @@ -134,7 +163,7 @@ AndroidShellHolder::AndroidShellHolder( } } }); - task_runners.GetUITaskRunner()->PostTask([]() { + shell_->GetTaskRunners().GetUITaskRunner()->PostTask([]() { if (::setpriority(PRIO_PROCESS, gettid(), -1) != 0) { FML_LOG(ERROR) << "Failed to set UI task runner priority"; } diff --git a/shell/platform/android/android_shell_holder.h b/shell/platform/android/android_shell_holder.h index 107e93f18672a..6fb6695801733 100644 --- a/shell/platform/android/android_shell_holder.h +++ b/shell/platform/android/android_shell_holder.h @@ -21,6 +21,14 @@ namespace flutter { class AndroidShellHolder { public: + // Whether the application sets to use embedded_view view + // `io.flutter.embedded_views_preview` flag. This can be static because it is + // determined by the application and it is safe when there are multiple + // `AndroidSurface`s. + // TODO(cyanglaz): remove this when dynamic thread merging is enabled on + // android. https://github.com/flutter/flutter/issues/59930 + static bool use_embedded_view; + AndroidShellHolder(flutter::Settings settings, std::shared_ptr jni_facade, bool is_background_view); diff --git a/shell/platform/android/android_surface_gl.cc b/shell/platform/android/android_surface_gl.cc index f4bd9e6747907..c8cc3d2d1183e 100644 --- a/shell/platform/android/android_surface_gl.cc +++ b/shell/platform/android/android_surface_gl.cc @@ -8,6 +8,7 @@ #include "flutter/fml/logging.h" #include "flutter/fml/memory/ref_ptr.h" +#include "flutter/shell/platform/android/android_shell_holder.h" namespace flutter { @@ -122,6 +123,9 @@ intptr_t AndroidSurfaceGL::GLContextFBO() const { // |GPUSurfaceGLDelegate| ExternalViewEmbedder* AndroidSurfaceGL::GetExternalViewEmbedder() { + if (!AndroidShellHolder::use_embedded_view) { + return nullptr; + } return external_view_embedder_.get(); } diff --git a/shell/platform/android/android_surface_software.cc b/shell/platform/android/android_surface_software.cc index 7face67038d0a..3bc52e317279a 100644 --- a/shell/platform/android/android_surface_software.cc +++ b/shell/platform/android/android_surface_software.cc @@ -11,6 +11,7 @@ #include "flutter/fml/platform/android/jni_weak_ref.h" #include "flutter/fml/platform/android/scoped_java_ref.h" #include "flutter/fml/trace_event.h" +#include "flutter/shell/platform/android/android_shell_holder.h" #include "flutter/shell/platform/android/jni/platform_view_android_jni.h" namespace flutter { @@ -145,6 +146,9 @@ bool AndroidSurfaceSoftware::PresentBackingStore( // |GPUSurfaceSoftwareDelegate| ExternalViewEmbedder* AndroidSurfaceSoftware::GetExternalViewEmbedder() { + if (!AndroidShellHolder::use_embedded_view) { + return nullptr; + } return external_view_embedder_.get(); } diff --git a/shell/platform/android/external_view_embedder/external_view_embedder.cc b/shell/platform/android/external_view_embedder/external_view_embedder.cc index 9485eedff5491..bec7d7c799d0d 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder.cc +++ b/shell/platform/android/external_view_embedder/external_view_embedder.cc @@ -63,10 +63,10 @@ SkRect AndroidExternalViewEmbedder::GetViewRect(int view_id) const { const EmbeddedViewParams& params = view_params_.at(view_id); // TODO(egarciad): The rect should be computed from the mutator stack. // https://github.com/flutter/flutter/issues/59821 - return SkRect::MakeXYWH(params.offsetPixels.x(), // - params.offsetPixels.y(), // - params.sizePoints.width() * device_pixel_ratio_, // - params.sizePoints.height() * device_pixel_ratio_ // + return SkRect::MakeXYWH(params.finalBoundingRect().x(), // + params.finalBoundingRect().y(), // + params.sizePoints().width() * device_pixel_ratio_, // + params.sizePoints().height() * device_pixel_ratio_ // ); } @@ -91,16 +91,6 @@ bool AndroidExternalViewEmbedder::SubmitFrame( for (size_t i = 0; i < composition_order_.size(); i++) { int64_t view_id = composition_order_[i]; - SkRect view_rect = GetViewRect(view_id); - - // Display the platform view. If it's already displayed, then it's - // just positioned and sized. - jni_facade_->FlutterViewOnDisplayPlatformView(view_id, // - view_rect.x(), // - view_rect.y(), // - view_rect.width(), // - view_rect.height() // - ); sk_sp picture = picture_recorders_.at(view_id)->finishRecordingAsPicture(); @@ -156,6 +146,15 @@ bool AndroidExternalViewEmbedder::SubmitFrame( frame->Submit(); for (int64_t view_id : composition_order_) { + SkRect view_rect = GetViewRect(view_id); + // Display the platform view. If it's already displayed, then it's + // just positioned and sized. + jni_facade_->FlutterViewOnDisplayPlatformView(view_id, // + view_rect.x(), // + view_rect.y(), // + view_rect.width(), // + view_rect.height() // + ); for (const SkRect& overlay_rect : overlay_layers.at(view_id)) { CreateSurfaceIfNeeded(context, // view_id, // @@ -255,8 +254,9 @@ void AndroidExternalViewEmbedder::CancelFrame() { // |ExternalViewEmbedder| void AndroidExternalViewEmbedder::EndFrame( + bool should_resubmit_frame, fml::RefPtr raster_thread_merger) { - if (should_run_rasterizer_on_platform_thread_) { + if (should_resubmit_frame && should_run_rasterizer_on_platform_thread_) { raster_thread_merger->MergeWithLease(kDefaultMergedLeaseDuration); should_run_rasterizer_on_platform_thread_ = false; } diff --git a/shell/platform/android/external_view_embedder/external_view_embedder.h b/shell/platform/android/external_view_embedder/external_view_embedder.h index 41b6e68f1155d..2f0562bdbf685 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder.h +++ b/shell/platform/android/external_view_embedder/external_view_embedder.h @@ -65,6 +65,7 @@ class AndroidExternalViewEmbedder final : public ExternalViewEmbedder { // |ExternalViewEmbedder| void EndFrame( + bool should_resubmit_frame, fml::RefPtr raster_thread_merger) override; // Gets the rect based on the device pixel ratio of a platform view displayed diff --git a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc index bba1c126564a1..8bac045e1b906 100644 --- a/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc +++ b/shell/platform/android/external_view_embedder/external_view_embedder_unittests.cc @@ -158,7 +158,7 @@ TEST(AndroidExternalViewEmbedder, RasterizerRunsOnPlatformThread) { ASSERT_TRUE(embedder->SubmitFrame(nullptr, nullptr)); EXPECT_CALL(*jni_mock, FlutterViewEndFrame()); - embedder->EndFrame(raster_thread_merger); + embedder->EndFrame(/*should_resubmit_frame=*/true, raster_thread_merger); ASSERT_TRUE(raster_thread_merger->IsMerged()); @@ -182,7 +182,7 @@ TEST(AndroidExternalViewEmbedder, RasterizerRunsOnRasterizerThread) { ASSERT_EQ(PostPrerollResult::kSuccess, result); EXPECT_CALL(*jni_mock, FlutterViewEndFrame()); - embedder->EndFrame(raster_thread_merger); + embedder->EndFrame(/*should_resubmit_frame=*/true, raster_thread_merger); ASSERT_FALSE(raster_thread_merger->IsMerged()); } @@ -198,9 +198,11 @@ TEST(AndroidExternalViewEmbedder, PlatformViewRect) { embedder->BeginFrame(SkISize::Make(100, 100), nullptr, 1.5, raster_thread_merger); - auto view_params = std::make_unique(); - view_params->offsetPixels = SkPoint::Make(10, 20); - view_params->sizePoints = SkSize::Make(30, 40); + MutatorsStack stack; + SkMatrix matrix = SkMatrix::MakeTrans(10, 20); + stack.PushTransform(matrix); + auto view_params = + std::make_unique(matrix, SkSize::Make(30, 40), stack); auto view_id = 0; embedder->PrerollCompositeEmbeddedView(view_id, std::move(view_params)); @@ -219,14 +221,21 @@ TEST(AndroidExternalViewEmbedder, PlatformViewRect__ChangedParams) { raster_thread_merger); auto view_id = 0; - auto view_params_1 = std::make_unique(); - view_params_1->offsetPixels = SkPoint::Make(10, 20); - view_params_1->sizePoints = SkSize::Make(30, 40); + + MutatorsStack stack1; + SkMatrix matrix1 = SkMatrix::MakeTrans(10, 20); + stack1.PushTransform(matrix1); + auto view_params_1 = std::make_unique( + matrix1, SkSize::Make(30, 40), stack1); + embedder->PrerollCompositeEmbeddedView(view_id, std::move(view_params_1)); - auto view_params_2 = std::make_unique(); - view_params_2->offsetPixels = SkPoint::Make(50, 60); - view_params_2->sizePoints = SkSize::Make(70, 80); + MutatorsStack stack2; + SkMatrix matrix2 = SkMatrix::MakeTrans(50, 60); + stack2.PushTransform(matrix2); + auto view_params_2 = std::make_unique( + matrix2, SkSize::Make(70, 80), stack2); + embedder->PrerollCompositeEmbeddedView(view_id, std::move(view_params_2)); ASSERT_EQ(SkRect::MakeXYWH(50, 60, 105, 120), embedder->GetViewRect(view_id)); @@ -281,11 +290,14 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame__RecycleSurfaces) { embedder->BeginFrame(frame_size, nullptr, 1.5, raster_thread_merger); // Add an Android view. - auto view_params_1 = std::make_unique(); - view_params_1->offsetPixels = SkPoint::Make(100, 100); + MutatorsStack stack1; + SkMatrix matrix1 = SkMatrix::MakeTrans(100, 100); + stack1.PushTransform(matrix1); // TODO(egarciad): Investigate why Flow applies the device pixel ratio to // the offsetPixels, but not the sizePoints. - view_params_1->sizePoints = SkSize::Make(200, 200); + auto view_params_1 = std::make_unique( + matrix1, SkSize::Make(200, 200), stack1); + embedder->PrerollCompositeEmbeddedView(0, std::move(view_params_1)); // This is the recording canvas flow writes to. auto canvas_1 = embedder->CompositeEmbeddedView(0); @@ -320,7 +332,7 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame__RecycleSurfaces) { embedder->SubmitFrame(gr_context.get(), std::move(surface_frame)); EXPECT_CALL(*jni_mock, FlutterViewEndFrame()); - embedder->EndFrame(raster_thread_merger); + embedder->EndFrame(/*should_resubmit_frame=*/false, raster_thread_merger); } // ------------------ Second frame ------------------ // @@ -329,11 +341,14 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame__RecycleSurfaces) { embedder->BeginFrame(frame_size, nullptr, 1.5, raster_thread_merger); // Add an Android view. - auto view_params_1 = std::make_unique(); - view_params_1->offsetPixels = SkPoint::Make(100, 100); + MutatorsStack stack1; + SkMatrix matrix1 = SkMatrix::MakeTrans(100, 100); + stack1.PushTransform(matrix1); // TODO(egarciad): Investigate why Flow applies the device pixel ratio to // the offsetPixels, but not the sizePoints. - view_params_1->sizePoints = SkSize::Make(200, 200); + auto view_params_1 = std::make_unique( + matrix1, SkSize::Make(200, 200), stack1); + embedder->PrerollCompositeEmbeddedView(0, std::move(view_params_1)); // This is the recording canvas flow writes to. auto canvas_1 = embedder->CompositeEmbeddedView(0); @@ -365,7 +380,7 @@ TEST(AndroidExternalViewEmbedder, SubmitFrame__RecycleSurfaces) { embedder->SubmitFrame(gr_context.get(), std::move(surface_frame)); EXPECT_CALL(*jni_mock, FlutterViewEndFrame()); - embedder->EndFrame(raster_thread_merger); + embedder->EndFrame(/*should_resubmit_frame=*/false, raster_thread_merger); } } @@ -384,7 +399,7 @@ TEST(AndroidExternalViewEmbedder, DoesNotCallJNIPlatformThreadOnlyMethods) { raster_thread_merger); EXPECT_CALL(*jni_mock, FlutterViewEndFrame()).Times(0); - embedder->EndFrame(raster_thread_merger); + embedder->EndFrame(/*should_resubmit_frame=*/false, raster_thread_merger); } } // namespace testing diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java b/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java new file mode 100644 index 0000000000000..6db0d93d0783c --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java @@ -0,0 +1,134 @@ +// 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.embedding.android; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorSpace; +import android.hardware.HardwareBuffer; +import android.media.Image; +import android.media.Image.Plane; +import android.media.ImageReader; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.embedding.engine.renderer.FlutterRenderer; +import io.flutter.embedding.engine.renderer.RenderSurface; + +/** + * Paints a Flutter UI provided by an {@link android.media.ImageReader} onto a {@link + * android.graphics.Canvas}. + * + *

A {@code FlutterImageView} is intended for situations where a developer needs to render a + * Flutter UI, but also needs to render an interactive {@link + * io.flutter.plugin.platform.PlatformView}. + * + *

This {@code View} takes an {@link android.media.ImageReader} that provides the Flutter UI in + * an {@link android.media.Image} and renders it to the {@link android.graphics.Canvas} in {@code + * onDraw}. + */ +@SuppressLint("ViewConstructor") +@TargetApi(19) +public class FlutterImageView extends View implements RenderSurface { + private final ImageReader imageReader; + @Nullable private Image nextImage; + @Nullable private Image currentImage; + @Nullable private Bitmap currentBitmap; + @Nullable private FlutterRenderer flutterRenderer; + + /** + * Constructs a {@code FlutterImageView} with an {@link android.media.ImageReader} that provides + * the Flutter UI. + */ + public FlutterImageView(@NonNull Context context, @NonNull ImageReader imageReader) { + super(context, null); + this.imageReader = imageReader; + } + + @Nullable + @Override + public FlutterRenderer getAttachedRenderer() { + return flutterRenderer; + } + + /** + * Invoked by the owner of this {@code FlutterImageView} when it wants to begin rendering a + * Flutter UI to this {@code FlutterImageView}. + */ + @Override + public void attachToRenderer(@NonNull FlutterRenderer flutterRenderer) { + if (this.flutterRenderer != null) { + this.flutterRenderer.stopRenderingToSurface(); + } + + this.flutterRenderer = flutterRenderer; + flutterRenderer.startRenderingToSurface(imageReader.getSurface()); + } + + /** + * Invoked by the owner of this {@code FlutterImageView} when it no longer wants to render a + * Flutter UI to this {@code FlutterImageView}. + */ + public void detachFromRenderer() { + if (flutterRenderer != null) { + flutterRenderer.stopRenderingToSurface(); + flutterRenderer = null; + } + } + + /** Acquires the next image to be drawn to the {@link android.graphics.Canvas}. */ + @TargetApi(19) + public void acquireLatestImage() { + nextImage = imageReader.acquireLatestImage(); + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (nextImage != null) { + if (currentImage != null) { + currentImage.close(); + } + currentImage = nextImage; + nextImage = null; + updateCurrentBitmap(); + } + + if (currentBitmap != null) { + canvas.drawBitmap(currentBitmap, 0, 0, null); + } + } + + @TargetApi(29) + private void updateCurrentBitmap() { + if (android.os.Build.VERSION.SDK_INT >= 29) { + final HardwareBuffer buffer = currentImage.getHardwareBuffer(); + currentBitmap = Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB)); + } else { + final Plane[] imagePlanes = currentImage.getPlanes(); + if (imagePlanes.length != 1) { + return; + } + + final Plane imagePlane = imagePlanes[0]; + final int desiredWidth = imagePlane.getRowStride() / imagePlane.getPixelStride(); + final int desiredHeight = currentImage.getHeight(); + + if (currentBitmap == null + || currentBitmap.getWidth() != desiredWidth + || currentBitmap.getHeight() != desiredHeight) { + currentBitmap = + Bitmap.createBitmap( + desiredWidth, desiredHeight, android.graphics.Bitmap.Config.ARGB_8888); + } + + currentBitmap.copyPixelsFromBuffer(imagePlane.getBuffer()); + } + } +} diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index c05b37bdb4e67..043097bc40a11 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -10,6 +10,7 @@ import android.content.res.Configuration; import android.graphics.Insets; import android.graphics.Rect; +import android.media.ImageReader; import android.os.Build; import android.text.format.DateFormat; import android.util.AttributeSet; @@ -79,6 +80,7 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC // Internal view hierarchy references. @Nullable private FlutterSurfaceView flutterSurfaceView; @Nullable private FlutterTextureView flutterTextureView; + @Nullable private FlutterImageView flutterImageView; @Nullable private RenderSurface renderSurface; private final Set flutterUiDisplayListeners = new HashSet<>(); private boolean isFlutterUiDisplayed; @@ -155,7 +157,8 @@ public FlutterView(@NonNull Context context) { /** * Deprecated - use {@link #FlutterView(Context, FlutterSurfaceView)} or {@link - * #FlutterView(Context, FlutterTextureView)} instead. + * #FlutterView(Context, FlutterTextureView)} or {@link #FlutterView(Context, FlutterImageView)} + * instead. */ @Deprecated public FlutterView(@NonNull Context context, @NonNull RenderMode renderMode) { @@ -164,9 +167,12 @@ public FlutterView(@NonNull Context context, @NonNull RenderMode renderMode) { if (renderMode == RenderMode.surface) { flutterSurfaceView = new FlutterSurfaceView(context); renderSurface = flutterSurfaceView; - } else { + } else if (renderMode == RenderMode.texture) { flutterTextureView = new FlutterTextureView(context); renderSurface = flutterTextureView; + } else { + throw new IllegalArgumentException( + String.format("RenderMode not supported with this constructor: %s", renderMode)); } init(); @@ -216,6 +222,18 @@ public FlutterView(@NonNull Context context, @NonNull FlutterTextureView flutter this(context, null, flutterTextureView); } + /** + * Constructs a {@code FlutterView} programmatically, without any XML attributes, uses the given + * {@link FlutterImageView} to render the Flutter UI. + * + *

{@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} to be + * compatible with {@link PlatformViewsController}. + */ + @TargetApi(19) + public FlutterView(@NonNull Context context, @NonNull FlutterImageView flutterImageView) { + this(context, null, flutterImageView); + } + /** * Constructs a {@code FlutterView} in an XML-inflation-compliant manner. * @@ -243,9 +261,12 @@ public FlutterView( flutterSurfaceView = new FlutterSurfaceView(context, transparencyMode == TransparencyMode.transparent); renderSurface = flutterSurfaceView; - } else { + } else if (renderMode == RenderMode.texture) { flutterTextureView = new FlutterTextureView(context); renderSurface = flutterTextureView; + } else { + throw new IllegalArgumentException( + String.format("RenderMode not supported with this constructor: %s", renderMode)); } init(); @@ -275,15 +296,31 @@ private FlutterView( init(); } + @TargetApi(19) + private FlutterView( + @NonNull Context context, + @Nullable AttributeSet attrs, + @NonNull FlutterImageView flutterImageView) { + super(context, attrs); + + this.flutterImageView = flutterImageView; + this.renderSurface = flutterImageView; + + init(); + } + private void init() { Log.v(TAG, "Initializing FlutterView"); if (flutterSurfaceView != null) { Log.v(TAG, "Internally using a FlutterSurfaceView."); addView(flutterSurfaceView); - } else { + } else if (flutterTextureView != null) { Log.v(TAG, "Internally using a FlutterTextureView."); addView(flutterTextureView); + } else { + Log.v(TAG, "Internally using a FlutterImageView."); + addView(flutterImageView); } // FlutterView needs to be focusable so that the InputMethodManager can interact with it. @@ -905,6 +942,32 @@ public void detachFromFlutterEngine() { flutterEngine = null; } + public void convertToImageView() { + renderSurface.detachFromRenderer(); + + ImageReader imageReader = PlatformViewsController.createImageReader(getWidth(), getHeight()); + flutterImageView = new FlutterImageView(getContext(), imageReader); + renderSurface = flutterImageView; + if (flutterEngine != null) { + renderSurface.attachToRenderer(flutterEngine.getRenderer()); + } + + removeAllViews(); + addView(flutterImageView); + + // TODO(jsimmons): this is a temporary hack that schedules a redraw of the FlutterImageView + // at a time when the engine has presumably posted a frame. Remove this when + // PlatformViewsController.onEndFrame callbacks have been implemented. + postDelayed( + new Runnable() { + public void run() { + flutterImageView.acquireLatestImage(); + flutterImageView.invalidate(); + } + }, + 1000); + } + /** Returns true if this {@code FlutterView} is currently attached to a {@link FlutterEngine}. */ @VisibleForTesting public boolean isAttachedToFlutterEngine() { @@ -1018,7 +1081,16 @@ public enum RenderMode { * android.graphics.SurfaceTexture} are required, developers should strongly prefer the {@link * RenderMode#surface} render mode. */ - texture + texture, + /** + * {@code RenderMode}, which paints Paints a Flutter UI provided by an {@link + * android.media.ImageReader} onto a {@link android.graphics.Canvas}. This mode is not as + * performant as {@link RenderMode#surface}, but a {@code FlutterView} in this mode can handle + * full interactivity with a {@link io.flutter.plugin.platform.PlatformView}. Unless {@link + * io.flutter.plugin.platform.PlatformView}s are required developers should strongly prefer the + * {@link RenderMode#surface} render mode. + */ + image } /** diff --git a/shell/platform/android/io/flutter/embedding/android/RenderMode.java b/shell/platform/android/io/flutter/embedding/android/RenderMode.java index 3c0907bb850a3..2471396178aec 100644 --- a/shell/platform/android/io/flutter/embedding/android/RenderMode.java +++ b/shell/platform/android/io/flutter/embedding/android/RenderMode.java @@ -21,5 +21,14 @@ public enum RenderMode { * Views}. Unless the special capabilities of a {@link android.graphics.SurfaceTexture} are * required, developers should strongly prefer the {@link #surface} render mode. */ - texture + texture, + /** + * {@code RenderMode}, which paints Paints a Flutter UI provided by an {@link + * android.media.ImageReader} onto a {@link android.graphics.Canvas}. This mode is not as + * performant as {@link RenderMode#surface}, but a {@code FlutterView} in this mode can handle + * full interactivity with a {@link io.flutter.plugin.platform.PlatformView}. Unless {@link + * io.flutter.plugin.platform.PlatformView}s are required developers should strongly prefer the + * {@link RenderMode#surface} render mode. + */ + image } diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterOverlaySurface.java b/shell/platform/android/io/flutter/embedding/engine/FlutterOverlaySurface.java index 05a3bdafb0c8c..64711d66cf26f 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterOverlaySurface.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterOverlaySurface.java @@ -8,12 +8,12 @@ import androidx.annotation.Keep; import androidx.annotation.NonNull; +@Keep public class FlutterOverlaySurface { @NonNull private final Surface surface; private final long id; - @Keep public FlutterOverlaySurface(long id, @NonNull Surface surface) { this.id = id; this.surface = surface; diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java index c57d8c86b8374..9156a64ef061e 100644 --- a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java @@ -201,7 +201,6 @@ public void ensureInitializationComplete( + applicationInfo.nativeLibraryDir + File.separator + DEFAULT_LIBRARY); - if (args != null) { Collections.addAll(shellArgs, args); } @@ -234,6 +233,17 @@ public void ensureInitializationComplete( } long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis; + + // TODO(cyanlaz): Remove this when dynamic thread merging is done. + // https://github.com/flutter/flutter/issues/59930 + Bundle bundle = applicationInfo.metaData; + if (bundle != null) { + boolean use_embedded_view = bundle.getBoolean("io.flutter.embedded_views_preview"); + if (use_embedded_view) { + shellArgs.add("--use-embedded-view"); + } + } + FlutterJNI.nativeInit( applicationContext, shellArgs.toArray(new String[0]), diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 706b31228a090..e0a409d8daa12 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -9,14 +9,20 @@ import android.annotation.TargetApi; import android.content.Context; +import android.graphics.PixelFormat; +import android.hardware.HardwareBuffer; +import android.media.ImageReader; import android.os.Build; import android.util.DisplayMetrics; import android.util.Log; +import android.util.LongSparseArray; import android.view.MotionEvent; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.android.FlutterImageView; +import io.flutter.embedding.android.FlutterView; import io.flutter.embedding.engine.FlutterOverlaySurface; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel; @@ -71,6 +77,15 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega // it is associated with(e.g if a platform view creates other views in the same virtual display. private final HashMap contextToPlatformView; + // Map of unique IDs to views that render overlay layers. + private final LongSparseArray overlayLayerViews; + + // Next available unique ID for use in overlayLayerViews. + private long nextOverlayLayerId = 0; + + // Tracks whether the flutterView has been converted to use a FlutterImageView. + private boolean flutterViewConvertedToImageView = false; + private final PlatformViewsChannel.PlatformViewsHandler channelHandler = new PlatformViewsChannel.PlatformViewsHandler() { @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @@ -283,6 +298,7 @@ public PlatformViewsController() { vdControllers = new HashMap<>(); accessibilityEventsDelegate = new AccessibilityEventsDelegate(); contextToPlatformView = new HashMap<>(); + overlayLayerViews = new LongSparseArray<>(); } /** @@ -540,7 +556,10 @@ public void onDisplayPlatformView(int viewId, int x, int y, int width, int heigh } public void onDisplayOverlaySurface(int id, int x, int y, int width, int height) { - // TODO: Implement this method. https://github.com/flutter/flutter/issues/58288 + if (!flutterViewConvertedToImageView) { + ((FlutterView) flutterView).convertToImageView(); + flutterViewConvertedToImageView = true; + } } public void onBeginFrame() { @@ -551,8 +570,27 @@ public void onEndFrame() { // TODO: Implement this method. https://github.com/flutter/flutter/issues/58288 } + @TargetApi(19) + public static ImageReader createImageReader(int width, int height) { + if (android.os.Build.VERSION.SDK_INT >= 29) { + return ImageReader.newInstance( + width, + height, + PixelFormat.RGBA_8888, + 2, + HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE | HardwareBuffer.USAGE_GPU_COLOR_OUTPUT); + } else { + return ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2); + } + } + + @TargetApi(19) public FlutterOverlaySurface createOverlaySurface() { - // TODO: Implement this method. https://github.com/flutter/flutter/issues/58288 - return null; + ImageReader imageReader = createImageReader(flutterView.getWidth(), flutterView.getHeight()); + FlutterImageView imageView = new FlutterImageView(flutterView.getContext(), imageReader); + long id = nextOverlayLayerId++; + overlayLayerViews.put(id, imageView); + + return new FlutterOverlaySurface(id, imageReader.getSurface()); } } diff --git a/shell/platform/android/platform_view_android.h b/shell/platform/android/platform_view_android.h index c0b6a1d79a4d8..c7b22fb17070e 100644 --- a/shell/platform/android/platform_view_android.h +++ b/shell/platform/android/platform_view_android.h @@ -117,7 +117,6 @@ class PlatformViewAndroid final : public PlatformView { FML_DISALLOW_COPY_AND_ASSIGN(PlatformViewAndroid); }; - } // namespace flutter #endif // SHELL_PLATFORM_ANDROID_PLATFORM_VIEW_ANDROID_H_ diff --git a/shell/platform/android/platform_view_android_jni_impl.cc b/shell/platform/android/platform_view_android_jni_impl.cc index 17239099d5a43..cebb2ecb02e18 100644 --- a/shell/platform/android/platform_view_android_jni_impl.cc +++ b/shell/platform/android/platform_view_android_jni_impl.cc @@ -102,6 +102,10 @@ static jmethodID g_on_display_platform_view_method = nullptr; static jmethodID g_on_display_overlay_surface_method = nullptr; +static jmethodID g_overlay_surface_id_method = nullptr; + +static jmethodID g_overlay_surface_surface_method = nullptr; + // Called By Java static jlong AttachJNI(JNIEnv* env, jclass clazz, @@ -698,6 +702,26 @@ bool RegisterApi(JNIEnv* env) { return false; } + fml::jni::ScopedJavaLocalRef overlay_surface_class( + env, env->FindClass("io/flutter/embedding/engine/FlutterOverlaySurface")); + if (overlay_surface_class.is_null()) { + FML_LOG(ERROR) << "Could not locate FlutterOverlaySurface class"; + return false; + } + g_overlay_surface_id_method = + env->GetMethodID(overlay_surface_class.obj(), "getId", "()J"); + if (g_overlay_surface_id_method == nullptr) { + FML_LOG(ERROR) << "Could not locate FlutterOverlaySurface#getId() method"; + return false; + } + g_overlay_surface_surface_method = env->GetMethodID( + overlay_surface_class.obj(), "getSurface", "()Landroid/view/Surface;"); + if (g_overlay_surface_surface_method == nullptr) { + FML_LOG(ERROR) + << "Could not locate FlutterOverlaySurface#getSurface() method"; + return false; + } + return true; } @@ -1113,12 +1137,27 @@ PlatformViewAndroidJNIImpl::FlutterViewCreateOverlaySurface() { return nullptr; } - env->CallVoidMethod(java_object.obj(), g_create_overlay_surface_method); - + fml::jni::ScopedJavaLocalRef overlay( + env, env->CallObjectMethod(java_object.obj(), + g_create_overlay_surface_method)); FML_CHECK(CheckException(env)); - // TODO(egarciad): Wire this up. - // https://github.com/flutter/flutter/issues/55270 - return std::make_unique(0, nullptr); + + if (overlay.is_null()) { + return std::make_unique(0, + nullptr); + } + + jlong overlay_id = + env->CallLongMethod(overlay.obj(), g_overlay_surface_id_method); + + jobject overlay_surface = + env->CallObjectMethod(overlay.obj(), g_overlay_surface_surface_method); + + auto overlay_window = fml::MakeRefCounted( + ANativeWindow_fromSurface(env, overlay_surface)); + + return std::make_unique( + overlay_id, std::move(overlay_window)); } std::unique_ptr> diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java index 7461674f243b0..ad29381ef273d 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java @@ -12,6 +12,7 @@ import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.media.ImageReader; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; @@ -370,6 +371,17 @@ public void systemInsetHandlesFullscreenNavbarLeft() { assertEquals(100, viewportMetricsCaptor.getValue().paddingRight); } + @Test + public void flutterImageView_acquiresImageAndInvalidates() { + final ImageReader mockReader = mock(ImageReader.class); + final FlutterImageView imageView = + spy(new FlutterImageView(RuntimeEnvironment.application, mockReader)); + + imageView.acquireLatestImage(); + verify(mockReader, times(1)).acquireLatestImage(); + verify(imageView, times(1)).invalidate(); + } + /* * A custom shadow that reports fullscreen flag for system UI visibility */ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 474e9f084c1ee..ff202af0215fd 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -163,13 +163,13 @@ NSObject* embedded_view = [factory createWithFrame:CGRectZero viewIdentifier:viewId arguments:params]; + UIView* platform_view = [embedded_view view]; // Set a unique view identifier, so the platform view can be identified in unit tests. - [embedded_view view].accessibilityIdentifier = - [NSString stringWithFormat:@"platform_view[%ld]", viewId]; + platform_view.accessibilityIdentifier = [NSString stringWithFormat:@"platform_view[%ld]", viewId]; views_[viewId] = fml::scoped_nsobject>([embedded_view retain]); FlutterTouchInterceptingView* touch_interceptor = [[[FlutterTouchInterceptingView alloc] - initWithEmbeddedView:embedded_view.view + initWithEmbeddedView:platform_view flutterViewController:flutter_view_controller_.get() gestureRecognizersBlockingPolicy:gesture_recognizers_blocking_policies[viewType]] autorelease]; @@ -458,7 +458,7 @@ } SkRect FlutterPlatformViewsController::GetPlatformViewRect(int view_id) { - UIView* platform_view = [views_[view_id].get() view]; + UIView* platform_view = [touch_interceptors_[view_id] embeddedView]; UIScreen* screen = [UIScreen mainScreen]; CGRect platform_view_cgrect = [platform_view convertRect:platform_view.bounds toView:flutter_view_]; @@ -604,8 +604,9 @@ } void FlutterPlatformViewsController::EndFrame( + bool should_resubmit_frame, fml::RefPtr raster_thread_merger) { - if (merge_threads_) { + if (should_resubmit_frame && merge_threads_) { raster_thread_merger->MergeWithLease(kDefaultMergedLeaseDuration); merge_threads_ = false; } @@ -731,6 +732,7 @@ - (instancetype)initWithTarget:(id)target @implementation FlutterTouchInterceptingView { fml::scoped_nsobject _delayingRecognizer; FlutterPlatformViewGestureRecognizersBlockingPolicy _blockingPolicy; + UIView* _embeddedView; } - (instancetype)initWithEmbeddedView:(UIView*)embeddedView flutterViewController:(UIViewController*)flutterViewController @@ -739,6 +741,7 @@ - (instancetype)initWithEmbeddedView:(UIView*)embeddedView self = [super initWithFrame:embeddedView.frame]; if (self) { self.multipleTouchEnabled = YES; + _embeddedView = embeddedView; embeddedView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); @@ -760,6 +763,10 @@ - (instancetype)initWithEmbeddedView:(UIView*)embeddedView return self; } +- (UIView*)embeddedView { + return _embeddedView; +} + - (void)releaseGesture { _delayingRecognizer.get().state = UIGestureRecognizerStateFailed; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 26984400c2ef2..66112faaf12bb 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -31,6 +31,9 @@ // Prevent the touch sequence from ever arriving to the embedded view. - (void)blockGesture; + +// Get embedded view +- (UIView*)embeddedView; @end // The parent view handles clipping to its subviews. @@ -168,7 +171,8 @@ class FlutterPlatformViewsController { // Invoked at the very end of a frame. // After invoking this method, nothing should happen on the current TaskRunner during the same // frame. - void EndFrame(fml::RefPtr raster_thread_merger); + void EndFrame(bool should_resubmit_frame, + fml::RefPtr raster_thread_merger); void OnMethodCall(FlutterMethodCall* call, FlutterResult& result); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index 20e36c4595d85..dc85fbee70692 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -325,7 +325,8 @@ - (void)setTextInputClient:(int)client { _textInputClient = client; } -- (void)setTextInputState:(NSDictionary*)state { +// Return true if the new input state needs to be synced back to the framework. +- (BOOL)setTextInputState:(NSDictionary*)state { NSString* newText = state[@"text"]; BOOL textChanged = ![self.text isEqualToString:newText]; if (textChanged) { @@ -356,8 +357,7 @@ - (void)setTextInputState:(NSDictionary*)state { selectedRange.length != oldSelectedRange.length) { needsEditingStateUpdate = YES; [self.inputDelegate selectionWillChange:self]; - [self setSelectedTextRange:[FlutterTextRange rangeWithNSRange:selectedRange] - updateEditingState:NO]; + [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]]; _selectionAffinity = _kTextAffinityDownstream; if ([state[@"selectionAffinity"] isEqualToString:@(_kTextAffinityUpstream)]) _selectionAffinity = _kTextAffinityUpstream; @@ -367,10 +367,9 @@ - (void)setTextInputState:(NSDictionary*)state { if (textChanged) { [self.inputDelegate textDidChange:self]; } - if (needsEditingStateUpdate) { - // For consistency with Android behavior, send an update to the framework. - [self updateEditingState]; - } + + // For consistency with Android behavior, send an update to the framework if anything changed. + return needsEditingStateUpdate; } - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text { @@ -401,11 +400,8 @@ - (UITextRange*)selectedTextRange { return [[_selectedTextRange copy] autorelease]; } -- (void)setSelectedTextRange:(UITextRange*)selectedTextRange { - [self setSelectedTextRange:selectedTextRange updateEditingState:YES]; -} - -- (void)setSelectedTextRange:(UITextRange*)selectedTextRange updateEditingState:(BOOL)update { +// Change the range of selected text, without notifying the framework. +- (void)setSelectedTextRangeLocal:(UITextRange*)selectedTextRange { if (_selectedTextRange != selectedTextRange) { UITextRange* oldSelectedRange = _selectedTextRange; if (self.hasText) { @@ -416,12 +412,14 @@ - (void)setSelectedTextRange:(UITextRange*)selectedTextRange updateEditingState: _selectedTextRange = [selectedTextRange copy]; } [oldSelectedRange release]; - - if (update) - [self updateEditingState]; } } +- (void)setSelectedTextRange:(UITextRange*)selectedTextRange { + [self setSelectedTextRangeLocal:selectedTextRange]; + [self updateEditingState]; +} + - (id)insertDictationResultPlaceholder { return @""; } @@ -440,26 +438,32 @@ - (NSString*)textInRange:(UITextRange*)range { return [self.text substringWithRange:textRange]; } -- (void)replaceRange:(UITextRange*)range withText:(NSString*)text { - NSRange replaceRange = ((FlutterTextRange*)range).range; +// Replace the text within the specified range with the given text, +// without notifying the framework. +- (void)replaceRangeLocal:(NSRange)range withText:(NSString*)text { NSRange selectedRange = _selectedTextRange.range; + // Adjust the text selection: // * reduce the length by the intersection length // * adjust the location by newLength - oldLength + intersectionLength - NSRange intersectionRange = NSIntersectionRange(replaceRange, selectedRange); - if (replaceRange.location <= selectedRange.location) - selectedRange.location += text.length - replaceRange.length; + NSRange intersectionRange = NSIntersectionRange(range, selectedRange); + if (range.location <= selectedRange.location) + selectedRange.location += text.length - range.length; if (intersectionRange.location != NSNotFound) { selectedRange.location += intersectionRange.length; selectedRange.length -= intersectionRange.length; } - [self.text replaceCharactersInRange:[self clampSelection:replaceRange forText:self.text] + [self.text replaceCharactersInRange:[self clampSelection:range forText:self.text] withString:text]; - [self setSelectedTextRange:[FlutterTextRange rangeWithNSRange:[self clampSelection:selectedRange - forText:self.text]] - updateEditingState:NO]; + [self setSelectedTextRangeLocal:[FlutterTextRange + rangeWithNSRange:[self clampSelection:selectedRange + forText:self.text]]]; +} +- (void)replaceRange:(UITextRange*)range withText:(NSString*)text { + NSRange replaceRange = ((FlutterTextRange*)range).range; + [self replaceRangeLocal:replaceRange withText:text]; [self updateEditingState]; } @@ -522,11 +526,11 @@ - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelecte if (markedTextRange.length > 0) { // Replace text in the marked range with the new text. - [self replaceRange:self.markedTextRange withText:markedText]; + [self replaceRangeLocal:markedTextRange withText:markedText]; markedTextRange.length = markedText.length; } else { // Replace text in the selected range with the new text. - [self replaceRange:_selectedTextRange withText:markedText]; + [self replaceRangeLocal:selectedRange withText:markedText]; markedTextRange = NSMakeRange(selectedRange.location, markedText.length); } @@ -535,9 +539,10 @@ - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelecte NSUInteger selectionLocation = markedSelectedRange.location + markedTextRange.location; selectedRange = NSMakeRange(selectionLocation, markedSelectedRange.length); - [self setSelectedTextRange:[FlutterTextRange rangeWithNSRange:[self clampSelection:selectedRange - forText:self.text]] - updateEditingState:YES]; + [self setSelectedTextRangeLocal:[FlutterTextRange + rangeWithNSRange:[self clampSelection:selectedRange + forText:self.text]]]; + [self updateEditingState]; } - (void)unmarkText { @@ -1002,7 +1007,9 @@ + (void)setupInputView:(FlutterTextInputView*)inputView } - (void)setTextInputEditingState:(NSDictionary*)state { - [_activeView setTextInputState:state]; + if ([_activeView setTextInputState:state]) { + [_activeView updateEditingState]; + } } - (void)clearTextInputClient { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m index 4f20c4d9da23e..2a33beedbab4b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m @@ -17,13 +17,27 @@ @interface FlutterTextInputView () - (void)setTextInputState:(NSDictionary*)state; @end -@implementation FlutterTextInputPluginTest -- (void)testSecureInput { - // Setup test. - id engine = OCMClassMock([FlutterEngine class]); - FlutterTextInputPlugin* textInputPlugin = [[FlutterTextInputPlugin alloc] init]; +@implementation FlutterTextInputPluginTest { + id engine; + FlutterTextInputPlugin* textInputPlugin; +} + +- (void)setUp { + [super setUp]; + + engine = OCMClassMock([FlutterEngine class]); + textInputPlugin = [[FlutterTextInputPlugin alloc] init]; textInputPlugin.textInputDelegate = engine; +} +- (void)tearDown { + [engine stopMocking]; + [[[[textInputPlugin textInputView] superview] subviews] + makeObjectsPerformSelector:@selector(removeFromSuperview)]; + [super tearDown]; +} + +- (void)testSecureInput { NSDictionary* config = @{ @"inputType" : @{@"name" : @"TextInuptType.text"}, @"keyboardAppearance" : @"Brightness.light", @@ -61,17 +75,9 @@ - (void)testSecureInput { // The one FlutterTextInputView we inserted into the view hierarchy should be the text input // plugin's active text input view. XCTAssertEqual(inputView, textInputPlugin.textInputView); - - // Clean up. - [engine stopMocking]; - [[[[textInputPlugin textInputView] superview] subviews] - makeObjectsPerformSelector:@selector(removeFromSuperview)]; } - (void)testTextChangesTriggerUpdateEditingClient { - // Setup test. - id engine = OCMClassMock([FlutterEngine class]); - FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; inputView.textInputDelegate = engine; @@ -86,15 +92,9 @@ - (void)testTextChangesTriggerUpdateEditingClient { // Don't send anything if there's nothing new. [inputView setTextInputState:@{@"text" : @"AFTER"}]; OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]); - - // Clean up. - [engine stopMocking]; } - (void)testSelectionChangeTriggersUpdateEditingClient { - // Setup test. - id engine = OCMClassMock([FlutterEngine class]); - FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; inputView.textInputDelegate = engine; @@ -118,15 +118,9 @@ - (void)testSelectionChangeTriggersUpdateEditingClient { [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @2}]; OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]); - - // Clean up. - [engine stopMocking]; } - (void)testComposingChangeTriggersUpdateEditingClient { - // Setup test. - id engine = OCMClassMock([FlutterEngine class]); - FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; inputView.textInputDelegate = engine; @@ -151,17 +145,9 @@ - (void)testComposingChangeTriggersUpdateEditingClient { [inputView setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}]; OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]); - - // Clean up. - [engine stopMocking]; } - (void)testAutofillInputViews { - // Setup test. - id engine = OCMClassMock([FlutterEngine class]); - FlutterTextInputPlugin* textInputPlugin = [[FlutterTextInputPlugin alloc] init]; - textInputPlugin.textInputDelegate = engine; - NSDictionary* template = @{ @"inputType" : @{@"name" : @"TextInuptType.text"}, @"keyboardAppearance" : @"Brightness.light", @@ -214,26 +200,15 @@ - (void)testAutofillInputViews { // Verify behavior. OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil] withTag:@"field2"]); - - // Clean up. - [engine stopMocking]; - [[[[textInputPlugin textInputView] superview] subviews] - makeObjectsPerformSelector:@selector(removeFromSuperview)]; } - (void)testAutocorrectionPromptRectAppears { - // Setup test. - id engine = OCMClassMock([FlutterEngine class]); - FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithFrame:CGRectZero]; inputView.textInputDelegate = engine; [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]]; // Verify behavior. OCMVerify([engine showAutocorrectionPromptRectForStart:0 end:1 withClient:0]); - - // Clean up mocks - [engine stopMocking]; } - (void)testTextRangeFromPositionMatchesUITextViewBehavior { @@ -248,4 +223,35 @@ - (void)testTextRangeFromPositionMatchesUITextViewBehavior { XCTAssertEqual(range.location, 0); XCTAssertEqual(range.length, 2); } + +- (void)testUITextInputCallsUpdateEditingStateOnce { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; + inputView.textInputDelegate = engine; + + __block int updateCount = 0; + OCMStub([engine updateEditingClient:0 withState:[OCMArg isNotNil]]) + .andDo(^(NSInvocation* invocation) { + updateCount++; + }); + + [inputView insertText:@"text to insert"]; + // Update the framework exactly once. + XCTAssertEqual(updateCount, 1); + + [inputView deleteBackward]; + XCTAssertEqual(updateCount, 2); + + inputView.selectedTextRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]; + XCTAssertEqual(updateCount, 3); + + [inputView replaceRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)] + withText:@"replace text"]; + XCTAssertEqual(updateCount, 4); + + [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)]; + XCTAssertEqual(updateCount, 5); + + [inputView unmarkText]; + XCTAssertEqual(updateCount, 6); +} @end diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm index 9f64a729a2dff..34694b55175ad 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm @@ -378,6 +378,11 @@ - (CGRect)globalRect { #pragma mark - UIAccessibilityElement protocol +- (void)setAccessibilityContainer:(id)container { + // Explicit noop. The containers are calculated lazily in `accessibilityContainer`. + // See also: https://github.com/flutter/flutter/issues/54366 +} + - (id)accessibilityContainer { if ([self hasChildren] || [self uid] == kRootNodeId) { if (_container == nil) diff --git a/shell/platform/darwin/ios/ios_surface.h b/shell/platform/darwin/ios/ios_surface.h index 5fb6f78f6d7cc..5e58ce2c6af4e 100644 --- a/shell/platform/darwin/ios/ios_surface.h +++ b/shell/platform/darwin/ios/ios_surface.h @@ -85,7 +85,8 @@ class IOSSurface : public ExternalViewEmbedder { bool SubmitFrame(GrContext* context, std::unique_ptr frame) override; // |ExternalViewEmbedder| - void EndFrame(fml::RefPtr raster_thread_merger) override; + void EndFrame(bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) override; public: FML_DISALLOW_COPY_AND_ASSIGN(IOSSurface); diff --git a/shell/platform/darwin/ios/ios_surface.mm b/shell/platform/darwin/ios/ios_surface.mm index 26eca6cf26b90..30b08c298c5b9 100644 --- a/shell/platform/darwin/ios/ios_surface.mm +++ b/shell/platform/darwin/ios/ios_surface.mm @@ -149,10 +149,11 @@ bool IsIosEmbeddedViewsPreviewEnabled() { } // |ExternalViewEmbedder| -void IOSSurface::EndFrame(fml::RefPtr raster_thread_merger) { +void IOSSurface::EndFrame(bool should_resubmit_frame, + fml::RefPtr raster_thread_merger) { TRACE_EVENT0("flutter", "IOSSurface::EndFrame"); FML_CHECK(platform_views_controller_ != nullptr); - return platform_views_controller_->EndFrame(raster_thread_merger); + return platform_views_controller_->EndFrame(should_resubmit_frame, raster_thread_merger); } } // namespace flutter diff --git a/testing/scenario_app/android/app/src/main/AndroidManifest.xml b/testing/scenario_app/android/app/src/main/AndroidManifest.xml index 65ff9e909329b..a7416851ccd0d 100644 --- a/testing/scenario_app/android/app/src/main/AndroidManifest.xml +++ b/testing/scenario_app/android/app/src/main/AndroidManifest.xml @@ -28,6 +28,9 @@ + diff --git a/tools/fuchsia/build_fuchsia_artifacts.py b/tools/fuchsia/build_fuchsia_artifacts.py index 2922b4068fb51..327a0d3fce6fa 100755 --- a/tools/fuchsia/build_fuchsia_artifacts.py +++ b/tools/fuchsia/build_fuchsia_artifacts.py @@ -215,14 +215,14 @@ def GetRunnerTarget(runner_type, product, aot): return base + target -def GetTargetsToBuild(product=False): +def GetTargetsToBuild(product=False, additional_targets=[]): targets_to_build = [ 'flutter/shell/platform/fuchsia:fuchsia', - ] + ] + additional_targets return targets_to_build -def BuildTarget(runtime_mode, arch, product, enable_lto): +def BuildTarget(runtime_mode, arch, product, enable_lto, additional_targets=[]): out_dir = 'fuchsia_%s_%s' % (runtime_mode, arch) flags = [ '--fuchsia', @@ -276,6 +276,12 @@ def main(): default=False, help='If set, skips building and just creates packages.') + parser.add_argument( + '--targets', + default='', + help=('Comma-separated list; adds additional targets to build for ' + 'Fuchsia.')) + args = parser.parse_args() RemoveDirectoryIfExists(_bucket_directory) build_mode = args.runtime_mode @@ -292,7 +298,8 @@ def main(): product = product_modes[i] if build_mode == 'all' or runtime_mode == build_mode: if not args.skip_build: - BuildTarget(runtime_mode, arch, product, enable_lto) + BuildTarget(runtime_mode, arch, product, enable_lto, + args.targets.split(",")) BuildBucket(runtime_mode, arch, product) if args.upload: