diff --git a/.ci.yaml b/.ci.yaml index 6b870a8d30d8a..ef6c1d59cc2ef 100755 --- a/.ci.yaml +++ b/.ci.yaml @@ -82,7 +82,7 @@ platform_properties: dependencies: >- [ {"dependency": "android_sdk", "version": "version:31v8"}, - {"dependency": "chrome_and_driver", "version": "version:84"}, + {"dependency": "chrome_and_driver", "version": "version:98"}, {"dependency": "open_jdk"} ] os: Mac-12 @@ -230,6 +230,7 @@ targets: properties: tags: > ["framework","hostonly","shard"] + scheduler: luci runIf: - .ci.yaml @@ -2388,6 +2389,17 @@ targets: task_name: android_choreographer_do_frame_test scheduler: luci + - name: Linux_android android_lifecycles_test + bringup: true + recipe: devicelab/devicelab_drone + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab","android","linux"] + task_name: android_lifecycles_test + scheduler: luci + - name: Mac build_aar_module_test recipe: devicelab/devicelab_drone timeout: 60 @@ -2447,7 +2459,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:31v8"}, - {"dependency": "chrome_and_driver", "version": "version:84"}, + {"dependency": "chrome_and_driver", "version": "version:98"}, {"dependency": "open_jdk"}, {"dependency": "xcode"}, {"dependency": "gems"}, @@ -2467,7 +2479,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:31v8"}, - {"dependency": "chrome_and_driver", "version": "version:84"}, + {"dependency": "chrome_and_driver", "version": "version:98"}, {"dependency": "open_jdk"}, {"dependency": "xcode"}, {"dependency": "gems"}, @@ -2487,7 +2499,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:31v8"}, - {"dependency": "chrome_and_driver", "version": "version:84"}, + {"dependency": "chrome_and_driver", "version": "version:98"}, {"dependency": "open_jdk"}, {"dependency": "xcode"}, {"dependency": "gems"}, @@ -2507,7 +2519,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:31v8"}, - {"dependency": "chrome_and_driver", "version": "version:84"}, + {"dependency": "chrome_and_driver", "version": "version:98"}, {"dependency": "open_jdk"}, {"dependency": "xcode"}, {"dependency": "gems"}, @@ -2553,6 +2565,30 @@ targets: - bin/** - .ci.yaml + - name: Mac entrypoint_dart_registrant + recipe: devicelab/devicelab_drone + bringup: true + timeout: 60 + properties: + caches: >- + [ + {"name":"gradle","path":"gradle"} + ] + dependencies: >- + [ + {"dependency": "xcode"}, + {"dependency": "gems"} + ] + tags: > + ["devicelab","hostonly"] + task_name: entrypoint_dart_registrant + scheduler: luci + runIf: + - dev/** + - packages/flutter_tools/** + - bin/** + - .ci.yaml + - name: Mac framework_tests_libraries recipe: flutter/flutter_drone timeout: 60 @@ -2916,7 +2952,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:31v8"}, - {"dependency": "chrome_and_driver", "version": "version:84"}, + {"dependency": "chrome_and_driver", "version": "version:98"}, {"dependency": "open_jdk"}, {"dependency": "xcode"}, {"dependency": "gems"}, @@ -2942,7 +2978,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:31v8"}, - {"dependency": "chrome_and_driver", "version": "version:84"}, + {"dependency": "chrome_and_driver", "version": "version:98"}, {"dependency": "open_jdk"}, {"dependency": "xcode"}, {"dependency": "gems"}, @@ -2968,7 +3004,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:31v8"}, - {"dependency": "chrome_and_driver", "version": "version:84"}, + {"dependency": "chrome_and_driver", "version": "version:98"}, {"dependency": "open_jdk"}, {"dependency": "xcode"}, {"dependency": "gems"}, @@ -2994,7 +3030,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:31v8"}, - {"dependency": "chrome_and_driver", "version": "version:84"}, + {"dependency": "chrome_and_driver", "version": "version:98"}, {"dependency": "open_jdk"}, {"dependency": "xcode"}, {"dependency": "gems"}, @@ -3074,7 +3110,7 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:31v8"}, - {"dependency": "chrome_and_driver", "version": "version:84"}, + {"dependency": "chrome_and_driver", "version": "version:98"}, {"dependency": "open_jdk"}, {"dependency": "xcode"}, {"dependency": "goldctl"} @@ -3083,8 +3119,6 @@ targets: subshard: web tags: > ["framework","hostonly","shard"] - os: Mac-10.15 # Override OS and Xcode for https://github.com/flutter/flutter/issues/98278 - xcode: 12c33 scheduler: luci runIf: - dev/** diff --git a/TESTOWNERS b/TESTOWNERS index 28bf447a581d1..94740d85f27a2 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -10,7 +10,9 @@ ## Linux Android DeviceLab tests /dev/devicelab/bin/tasks/analyzer_benchmark.dart @zanderso @flutter/tool +/dev/devicelab/bin/tasks/android_choreographer_do_frame_test.dart @blasten @flutter/engine /dev/devicelab/bin/tasks/android_defines_test.dart @zanderso @flutter/tool +/dev/devicelab/bin/tasks/android_lifecycles_test.dart @blasten @flutter/engine /dev/devicelab/bin/tasks/android_obfuscate_test.dart @zanderso @flutter/tool /dev/devicelab/bin/tasks/android_picture_cache_complexity_scoring_perf__timeline_summary.dart @flar @flutter/engine /dev/devicelab/bin/tasks/android_stack_size_test.dart @zanderso @flutter/tool @@ -71,7 +73,6 @@ /dev/devicelab/bin/tasks/opacity_peephole_fade_transition_text_perf__e2e_summary.dart @flar @flutter/engine /dev/devicelab/bin/tasks/opacity_peephole_col_of_alpha_savelayer_rows_perf__e2e_summary.dart @flar @flutter/engine /dev/devicelab/bin/tasks/opacity_peephole_grid_of_alpha_savelayers_perf__e2e_summary.dart @flar @flutter/engine -/dev/devicelab/bin/tasks/android_choreographer_do_frame_test.dart @blasten @flutter/engine ## Windows Android DeviceLab tests /dev/devicelab/bin/tasks/basic_material_app_win__compile.dart @zanderso @flutter/tool @@ -197,6 +198,7 @@ /dev/devicelab/bin/tasks/gradle_plugin_light_apk_test.dart @stuartmorgan @flutter/plugin /dev/devicelab/bin/tasks/module_test_ios.dart @jmagman @flutter/tool /dev/devicelab/bin/tasks/plugin_lint_mac.dart @stuartmorgan @flutter/plugin +/dev/devicelab/bin/tasks/entrypoint_dart_registrant.dart @aaclarke @flutter/plugin ## Host only framework tests # Linux analyze diff --git a/bin/internal/engine.version b/bin/internal/engine.version index bd06fb538b65d..f059a71ee0cf3 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -dc1dba505c3367090451ce01fb5a9d65542e9c48 +a5718ab3c33bc47cbc30f599f1d3e570fb4361d7 diff --git a/bin/internal/fuchsia-linux.version b/bin/internal/fuchsia-linux.version index 16b9ee12ff4d8..995491eba8527 100644 --- a/bin/internal/fuchsia-linux.version +++ b/bin/internal/fuchsia-linux.version @@ -1 +1 @@ -_mwyhkd9Hjv0o4KlgezBRadEtweO_9n5TNBYy7eaKgQC +7rB0YrprsDcIdiq5bueZWnkFa0BN34UgYLYHOR_vOHgC diff --git a/bin/internal/fuchsia-mac.version b/bin/internal/fuchsia-mac.version index 44fcc6ea2fdd6..59f29fda9d4c1 100644 --- a/bin/internal/fuchsia-mac.version +++ b/bin/internal/fuchsia-mac.version @@ -1 +1 @@ -qGsRXn7-DuBrrYDP3CixhkSD-wp2xF_dSAvfmqlMwgwC +RIOZP5nXz9S8_TKqJyRgjvxBVYxhXNyjrtQ_RxPY3wMC diff --git a/dev/devicelab/bin/tasks/android_lifecycles_test.dart b/dev/devicelab/bin/tasks/android_lifecycles_test.dart new file mode 100644 index 0000000000000..d4264b537a029 --- /dev/null +++ b/dev/devicelab/bin/tasks/android_lifecycles_test.dart @@ -0,0 +1,10 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/android_lifecycles_test.dart'; + +Future main() async { + await task(androidLifecyclesTest()); +} diff --git a/dev/devicelab/bin/tasks/entrypoint_dart_registrant.dart b/dev/devicelab/bin/tasks/entrypoint_dart_registrant.dart new file mode 100644 index 0000000000000..b4e91beb4bef7 --- /dev/null +++ b/dev/devicelab/bin/tasks/entrypoint_dart_registrant.dart @@ -0,0 +1,12 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/entrypoint_dart_registrant.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + await task(entrypointDartRegistrant()); +} diff --git a/dev/devicelab/lib/framework/devices.dart b/dev/devicelab/lib/framework/devices.dart index 5faa636b551fe..dd30e9a02f81e 100644 --- a/dev/devicelab/lib/framework/devices.dart +++ b/dev/devicelab/lib/framework/devices.dart @@ -545,7 +545,7 @@ class AndroidDevice extends Device { } } - /// Executes [command] on `adb shell` and returns its exit code. + /// Executes [command] on `adb shell`. Future shellExec(String command, List arguments, { Map? environment, bool silent = false }) async { await adb(['shell', command, ...arguments], environment: environment, silent: silent); } @@ -637,7 +637,7 @@ class AndroidDevice extends Device { late final StreamController stream; stream = StreamController( onListen: () async { - await adb(['logcat', '--clear']); + await adb(['logcat', '-c']); final Process process = await startProcess( adbPath, // Make logcat less chatty by filtering down to just ActivityManager diff --git a/dev/devicelab/lib/tasks/android_choreographer_do_frame_test.dart b/dev/devicelab/lib/tasks/android_choreographer_do_frame_test.dart index 8226c0a920519..d55b11e29229e 100644 --- a/dev/devicelab/lib/tasks/android_choreographer_do_frame_test.dart +++ b/dev/devicelab/lib/tasks/android_choreographer_do_frame_test.dart @@ -18,12 +18,11 @@ const List kSentinelStr = [ '==== sentinel #3 ====', ]; -// Regression test for https://github.com/flutter/flutter/issues/98973 -// This test ensures that Choreographer#doFrame finishes during application startup. -// This test fails if the application hangs during this period. -// https://ui.perfetto.dev/#!/?s=da6628c3a92456ae8fa3f345d0186e781da77e90fc8a64d073e9fee11d1e65 +/// Tests that Choreographer#doFrame finishes during application startup. +/// This test fails if the application hangs during this period. +/// https://ui.perfetto.dev/#!/?s=da6628c3a92456ae8fa3f345d0186e781da77e90fc8a64d073e9fee11d1e65 +/// Regression test for https://github.com/flutter/flutter/issues/98973 TaskFunction androidChoreographerDoFrameTest({ - String? deviceIdOverride, Map? environment, }) { final Directory tempDir = Directory.systemTemp @@ -87,18 +86,19 @@ Future main() async { } section('Flutter run (mode: $mode)'); + late Process run; await inDirectory(path.join(tempDir.path, 'app'), () async { - final Process run = await startProcess( + run = await startProcess( path.join(flutterDirectory.path, 'bin', 'flutter'), flutterCommandArgs('run', ['--$mode', '--verbose']), ); + }); - int currSentinelIdx = 0; - final StreamSubscription stdout = run.stdout - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((String line) { - + int currSentinelIdx = 0; + final StreamSubscription stdout = run.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { if (currSentinelIdx < sentinelCompleters.keys.length && line.contains(sentinelCompleters.keys.elementAt(currSentinelIdx))) { sentinelCompleters.values.elementAt(currSentinelIdx).complete(); @@ -107,61 +107,59 @@ Future main() async { } else { print('stdout: $line'); } - }); - final StreamSubscription stderr = run.stderr - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((String line) { - print('stderr: $line'); - }); - - final Completer exitCompleter = Completer(); - - unawaited(run.exitCode.then((int exitCode) { - exitCompleter.complete(); - })); + final StreamSubscription stderr = run.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + print('stderr: $line'); + }); - section('Wait for sentinels (mode: $mode)'); - for (final Completer completer in sentinelCompleters.values) { - if (nextCompleterIdx == 0) { - // Don't time out because we don't know how long it would take to get the first log. + final Completer exitCompleter = Completer(); + + unawaited(run.exitCode.then((int exitCode) { + exitCompleter.complete(); + })); + + section('Wait for sentinels (mode: $mode)'); + for (final Completer completer in sentinelCompleters.values) { + if (nextCompleterIdx == 0) { + // Don't time out because we don't know how long it would take to get the first log. + await Future.any( + >[ + completer.future, + exitCompleter.future, + ], + ); + } else { + try { + // Time out since this should not take 1s after the first log was received. await Future.any( >[ - completer.future, + completer.future.timeout(const Duration(seconds: 1)), exitCompleter.future, ], ); - } else { - try { - // Time out since this should not take 1s after the first log was received. - await Future.any( - >[ - completer.future.timeout(const Duration(seconds: 1)), - exitCompleter.future, - ], - ); - } on TimeoutException { - break; - } - } - if (exitCompleter.isCompleted) { - // The process exited. + } on TimeoutException { break; } - nextCompleterIdx++; } + if (exitCompleter.isCompleted) { + // The process exited. + break; + } + nextCompleterIdx++; + } - section('Quit app (mode: $mode)'); - run.stdin.write('q'); - await exitCompleter.future; + section('Quit app (mode: $mode)'); + run.stdin.write('q'); + await exitCompleter.future; - section('Stop listening to stdout and stderr (mode: $mode)'); - await stdout.cancel(); - await stderr.cancel(); - run.kill(); - }); + section('Stop listening to stdout and stderr (mode: $mode)'); + await stdout.cancel(); + await stderr.cancel(); + run.kill(); if (nextCompleterIdx == sentinelCompleters.values.length) { return TaskResult.success(null); diff --git a/dev/devicelab/lib/tasks/android_lifecycles_test.dart b/dev/devicelab/lib/tasks/android_lifecycles_test.dart new file mode 100644 index 0000000000000..91ba64a5c6bfd --- /dev/null +++ b/dev/devicelab/lib/tasks/android_lifecycles_test.dart @@ -0,0 +1,212 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import '../framework/devices.dart'; +import '../framework/framework.dart'; +import '../framework/task_result.dart'; +import '../framework/utils.dart'; + +const String _kOrgName = 'com.example.activitydestroy'; + +final RegExp _lifecycleSentinelRegExp = RegExp(r'==== lifecycle\: (.+) ===='); + +/// Tests the following Android lifecycles: Activity#onStop(), Activity#onResume(), Activity#onPause() +/// from Dart perspective in debug, profile, and release modes. +TaskFunction androidLifecyclesTest({ + Map? environment, +}) { + final Directory tempDir = Directory.systemTemp + .createTempSync('flutter_devicelab_activity_destroy.'); + return () async { + try { + section('Create app'); + await inDirectory(tempDir, () async { + await flutter( + 'create', + options: [ + '--platforms', + 'android', + '--org', + _kOrgName, + 'app', + ], + environment: environment, + ); + }); + + final File mainDart = File(path.join( + tempDir.absolute.path, + 'app', + 'lib', + 'main.dart', + )); + if (!mainDart.existsSync()) { + return TaskResult.failure('${mainDart.path} does not exist'); + } + + section('Patch lib/main.dart'); + await mainDart.writeAsString(r''' + import 'package:flutter/widgets.dart'; + +class LifecycleObserver extends WidgetsBindingObserver { + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + print('==== lifecycle: $state ===='); + } +} + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + WidgetsBinding.instance.addObserver(LifecycleObserver()); + runApp(Container()); +} +''', flush: true); + + final List androidLifecycles = []; + + Future runTestFor(String mode) async { + section('Flutter run (mode: $mode)'); + + late Process run; + await inDirectory(path.join(tempDir.path, 'app'), () async { + run = await startProcess( + path.join(flutterDirectory.path, 'bin', 'flutter'), + flutterCommandArgs('run', ['--$mode']), + ); + }); + + final AndroidDevice device = await devices.workingDevice as AndroidDevice; + await device.unlock(); + + final StreamController lifecyles = StreamController(); + + final StreamSubscription stdout = run.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String log) { + final RegExpMatch? match = _lifecycleSentinelRegExp.firstMatch(log); + print('stdout: $log'); + if (match == null) { + return; + } + final String lifecycle = match[1]!; + androidLifecycles.add(lifecycle); + + print('stdout: Found app lifecycle: $lifecycle'); + lifecyles.add(lifecycle); + }); + + final StreamSubscription stderr = run.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String log) { + print('stderr: $log'); + }); + + final StreamIterator lifecycleItr = StreamIterator(lifecyles.stream); + + { + const String expected = 'AppLifecycleState.resumed'; + await lifecycleItr.moveNext(); + final String got = lifecycleItr.current; + if (expected != got) { + return TaskResult.failure('expected lifecycles: `$expected`, but got` $got`'); + } + } + + section('Toggling app switch (mode: $mode)'); + await device.shellExec('input', ['keyevent', 'KEYCODE_APP_SWITCH']); + + { + const String expected = 'AppLifecycleState.inactive'; + await lifecycleItr.moveNext(); + final String got = lifecycleItr.current; + if (expected != got) { + return TaskResult.failure('expected lifecycles: `$expected`, but got` $got`'); + } + } + + section('Bring activity to foreground (mode: $mode)'); + await device.shellExec('am', ['start', '--activity-single-top', '$_kOrgName.app/.MainActivity']); + + { + const String expected = 'AppLifecycleState.resumed'; + await lifecycleItr.moveNext(); + final String got = lifecycleItr.current; + if (expected != got) { + return TaskResult.failure('expected lifecycles: `$expected`, but got` $got`'); + } + } + + section('Launch Settings app (mode: $mode)'); + await device.shellExec('am', ['start', '-a', 'android.settings.SETTINGS']); + + { + const String expected = 'AppLifecycleState.inactive'; + await lifecycleItr.moveNext(); + final String got = lifecycleItr.current; + if (expected != got) { + return TaskResult.failure('expected lifecycles: `$expected`, but got` $got`'); + } + } + + { + const String expected = 'AppLifecycleState.paused'; + await lifecycleItr.moveNext(); + final String got = lifecycleItr.current; + if (expected != got) { + return TaskResult.failure('expected lifecycles: `$expected`, but got` $got`'); + } + } + + section('Bring activity to foreground (mode: $mode)'); + await device.shellExec('am', ['start', '--activity-single-top', '$_kOrgName.app/.MainActivity']); + + { + const String expected = 'AppLifecycleState.resumed'; + await lifecycleItr.moveNext(); + final String got = lifecycleItr.current; + if (expected != got) { + return TaskResult.failure('expected lifecycles: `$expected`, but got` $got`'); + } + } + + run.kill(); + + section('Stop subscriptions (mode: $mode)'); + + await lifecycleItr.cancel(); + await lifecyles.close(); + await stdout.cancel(); + await stderr.cancel(); + return TaskResult.success(null); + } + + final TaskResult debugResult = await runTestFor('debug'); + if (debugResult.failed) { + return debugResult; + } + + final TaskResult profileResult = await runTestFor('profile'); + if (profileResult.failed) { + return profileResult; + } + + final TaskResult releaseResult = await runTestFor('release'); + if (releaseResult.failed) { + return releaseResult; + } + + return TaskResult.success(null); + } finally { + rmTree(tempDir); + } + }; +} diff --git a/dev/devicelab/lib/tasks/entrypoint_dart_registrant.dart b/dev/devicelab/lib/tasks/entrypoint_dart_registrant.dart new file mode 100644 index 0000000000000..a597b0f9c637f --- /dev/null +++ b/dev/devicelab/lib/tasks/entrypoint_dart_registrant.dart @@ -0,0 +1,106 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' show Process, ProcessSignal, Directory, File; + +import '../framework/devices.dart'; +import '../framework/framework.dart'; +import '../framework/task_result.dart'; +import '../framework/utils.dart'; + +const String _messagePrefix = 'entrypoint:'; +const String _entrypointName = 'entrypoint'; + +const String _dartCode = ''' +import 'package:flutter/widgets.dart'; + +@pragma('vm:entry-point') +void main() { + print('$_messagePrefix main'); + runApp(const ColoredBox(color: Color(0xffcc0000))); +} + +@pragma('vm:entry-point') +void $_entrypointName() { + print('$_messagePrefix $_entrypointName'); + runApp(const ColoredBox(color: Color(0xff00cc00))); +} +'''; + +const String _kotlinCode = ''' +package com.example.entrypoint_dart_registrant + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { + override fun getDartEntrypointFunctionName(): String { + return "$_entrypointName" + } +} +'''; + +Future _runWithTempDir(Directory tempDir) async { + const String testDirName = 'entrypoint_dart_registrant'; + final String testPath = '${tempDir.path}/$testDirName'; + await inDirectory(tempDir, () async { + await flutter('create', options: [ + '--platforms', + 'android', + testDirName, + ]); + }); + final String mainPath = '${tempDir.path}/$testDirName/lib/main.dart'; + print(mainPath); + File(mainPath).writeAsStringSync(_dartCode); + final String activityPath = + '${tempDir.path}/$testDirName/android/app/src/main/kotlin/com/example/entrypoint_dart_registrant/MainActivity.kt'; + File(activityPath).writeAsStringSync(_kotlinCode); + final Device device = await devices.workingDevice; + await device.unlock(); + final String entrypoint = await inDirectory(testPath, () async { + // The problem only manifested when the dart plugin registrant was used + // (which path_provider has). + await flutter('pub', options: ['add', 'path_provider:2.0.9']); + // The problem only manifested on release builds, so we test release. + final Process process = + await startFlutter('run', options: ['--release']); + final Completer completer = Completer(); + final StreamSubscription stdoutSub = process.stdout + .transform(const Utf8Decoder()) + .transform(const LineSplitter()) + .listen((String line) async { + print(line); + if (line.contains(_messagePrefix)) { + completer.complete(line); + } + }); + final String entrypoint = await completer.future; + await stdoutSub.cancel(); + process.stdin.write('q'); + await process.stdin.flush(); + process.kill(ProcessSignal.sigint); + return entrypoint; + }); + if (entrypoint.contains('$_messagePrefix $_entrypointName')) { + return TaskResult.success(null); + } else { + return TaskResult.failure('expected entrypoint:"$_entrypointName" but found:"$entrypoint"'); + } +} + +/// Asserts that the custom entrypoint works in the presence of the dart plugin +/// registrant. +TaskFunction entrypointDartRegistrant() { + return () async { + final Directory tempDir = + Directory.systemTemp.createTempSync('entrypoint_dart_registrant.'); + try { + return await _runWithTempDir(tempDir); + } finally { + rmTree(tempDir); + } + }; +} diff --git a/packages/flutter/lib/src/cupertino/desktop_text_selection.dart b/packages/flutter/lib/src/cupertino/desktop_text_selection.dart index e2c2d88f5c0d9..23e600f163871 100644 --- a/packages/flutter/lib/src/cupertino/desktop_text_selection.dart +++ b/packages/flutter/lib/src/cupertino/desktop_text_selection.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -49,15 +48,15 @@ class _CupertinoDesktopTextSelectionControls extends TextSelectionControls { Offset selectionMidpoint, List endpoints, TextSelectionDelegate delegate, - ValueListenable? clipboardStatus, + ClipboardStatusNotifier clipboardStatus, Offset? lastSecondaryTapDownPosition, ) { return _CupertinoDesktopTextSelectionControlsToolbar( clipboardStatus: clipboardStatus, endpoints: endpoints, globalEditableRegion: globalEditableRegion, - handleCut: canCut(delegate) ? () => handleCut(delegate) : null, - handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null, + handleCut: canCut(delegate) ? () => handleCut(delegate, clipboardStatus) : null, + handleCopy: canCopy(delegate) ? () => handleCopy(delegate, clipboardStatus) : null, handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null, handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null, selectionMidpoint: selectionMidpoint, @@ -99,7 +98,7 @@ class _CupertinoDesktopTextSelectionControlsToolbar extends StatefulWidget { required this.lastSecondaryTapDownPosition, }) : super(key: key); - final ValueListenable? clipboardStatus; + final ClipboardStatusNotifier? clipboardStatus; final List endpoints; final Rect globalEditableRegion; final VoidCallback? handleCopy; @@ -115,6 +114,8 @@ class _CupertinoDesktopTextSelectionControlsToolbar extends StatefulWidget { } class _CupertinoDesktopTextSelectionControlsToolbarState extends State<_CupertinoDesktopTextSelectionControlsToolbar> { + ClipboardStatusNotifier? _clipboardStatus; + void _onChangedClipboardStatus() { setState(() { // Inform the widget that the value of clipboardStatus has changed. @@ -124,28 +125,46 @@ class _CupertinoDesktopTextSelectionControlsToolbarState extends State<_Cupertin @override void initState() { super.initState(); - widget.clipboardStatus?.addListener(_onChangedClipboardStatus); + if (widget.handlePaste != null) { + _clipboardStatus = widget.clipboardStatus ?? ClipboardStatusNotifier(); + _clipboardStatus!.addListener(_onChangedClipboardStatus); + _clipboardStatus!.update(); + } } @override void didUpdateWidget(_CupertinoDesktopTextSelectionControlsToolbar oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.clipboardStatus != widget.clipboardStatus) { - oldWidget.clipboardStatus?.removeListener(_onChangedClipboardStatus); - widget.clipboardStatus?.addListener(_onChangedClipboardStatus); + if (_clipboardStatus != null) { + _clipboardStatus!.removeListener(_onChangedClipboardStatus); + _clipboardStatus!.dispose(); + } + _clipboardStatus = widget.clipboardStatus ?? ClipboardStatusNotifier(); + _clipboardStatus!.addListener(_onChangedClipboardStatus); + if (widget.handlePaste != null) { + _clipboardStatus!.update(); + } } } @override void dispose() { super.dispose(); - widget.clipboardStatus?.removeListener(_onChangedClipboardStatus); + // When used in an Overlay, this can be disposed after its creator has + // already disposed _clipboardStatus. + if (_clipboardStatus != null && !_clipboardStatus!.disposed) { + _clipboardStatus!.removeListener(_onChangedClipboardStatus); + if (widget.clipboardStatus == null) { + _clipboardStatus!.dispose(); + } + } } @override Widget build(BuildContext context) { // Don't render the menu until the state of the clipboard is known. - if (widget.handlePaste != null && widget.clipboardStatus?.value == ClipboardStatus.unknown) { + if (widget.handlePaste != null && _clipboardStatus!.value == ClipboardStatus.unknown) { return const SizedBox(width: 0.0, height: 0.0); } @@ -187,7 +206,7 @@ class _CupertinoDesktopTextSelectionControlsToolbarState extends State<_Cupertin addToolbarButton(localizations.copyButtonLabel, widget.handleCopy!); } if (widget.handlePaste != null - && widget.clipboardStatus?.value == ClipboardStatus.pasteable) { + && _clipboardStatus!.value == ClipboardStatus.pasteable) { addToolbarButton(localizations.pasteButtonLabel, widget.handlePaste!); } if (widget.handleSelectAll != null) { diff --git a/packages/flutter/lib/src/cupertino/text_selection.dart b/packages/flutter/lib/src/cupertino/text_selection.dart index 24b8c295fc9f5..69376ad842b2a 100644 --- a/packages/flutter/lib/src/cupertino/text_selection.dart +++ b/packages/flutter/lib/src/cupertino/text_selection.dart @@ -4,7 +4,6 @@ import 'dart:math' as math; -import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -38,7 +37,7 @@ class _CupertinoTextSelectionControlsToolbar extends StatefulWidget { required this.textLineHeight, }) : super(key: key); - final ValueListenable? clipboardStatus; + final ClipboardStatusNotifier? clipboardStatus; final List endpoints; final Rect globalEditableRegion; final VoidCallback? handleCopy; @@ -53,6 +52,8 @@ class _CupertinoTextSelectionControlsToolbar extends StatefulWidget { } class _CupertinoTextSelectionControlsToolbarState extends State<_CupertinoTextSelectionControlsToolbar> { + ClipboardStatusNotifier? _clipboardStatus; + void _onChangedClipboardStatus() { setState(() { // Inform the widget that the value of clipboardStatus has changed. @@ -62,28 +63,47 @@ class _CupertinoTextSelectionControlsToolbarState extends State<_CupertinoTextSe @override void initState() { super.initState(); - widget.clipboardStatus?.addListener(_onChangedClipboardStatus); + if (widget.handlePaste != null) { + _clipboardStatus = widget.clipboardStatus ?? ClipboardStatusNotifier(); + _clipboardStatus!.addListener(_onChangedClipboardStatus); + _clipboardStatus!.update(); + } } @override void didUpdateWidget(_CupertinoTextSelectionControlsToolbar oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.clipboardStatus != widget.clipboardStatus) { - oldWidget.clipboardStatus?.removeListener(_onChangedClipboardStatus); - widget.clipboardStatus?.addListener(_onChangedClipboardStatus); + if (_clipboardStatus != null) { + _clipboardStatus!.removeListener(_onChangedClipboardStatus); + _clipboardStatus!.dispose(); + } + _clipboardStatus = widget.clipboardStatus ?? ClipboardStatusNotifier(); + _clipboardStatus!.addListener(_onChangedClipboardStatus); + if (widget.handlePaste != null) { + _clipboardStatus!.update(); + } } } @override void dispose() { super.dispose(); - widget.clipboardStatus?.removeListener(_onChangedClipboardStatus); + // When used in an Overlay, this can be disposed after its creator has + // already disposed _clipboardStatus. + if (_clipboardStatus != null && !_clipboardStatus!.disposed) { + _clipboardStatus!.removeListener(_onChangedClipboardStatus); + if (widget.clipboardStatus == null) { + _clipboardStatus!.dispose(); + } + } } @override Widget build(BuildContext context) { // Don't render the menu until the state of the clipboard is known. - if (widget.handlePaste != null && widget.clipboardStatus?.value == ClipboardStatus.unknown) { + if (widget.handlePaste != null + && _clipboardStatus!.value == ClipboardStatus.unknown) { return const SizedBox(width: 0.0, height: 0.0); } @@ -137,7 +157,7 @@ class _CupertinoTextSelectionControlsToolbarState extends State<_CupertinoTextSe addToolbarButton(localizations.copyButtonLabel, widget.handleCopy!); } if (widget.handlePaste != null - && widget.clipboardStatus?.value == ClipboardStatus.pasteable) { + && _clipboardStatus!.value == ClipboardStatus.pasteable) { addToolbarButton(localizations.pasteButtonLabel, widget.handlePaste!); } if (widget.handleSelectAll != null) { @@ -209,15 +229,15 @@ class CupertinoTextSelectionControls extends TextSelectionControls { Offset selectionMidpoint, List endpoints, TextSelectionDelegate delegate, - ValueListenable? clipboardStatus, + ClipboardStatusNotifier clipboardStatus, Offset? lastSecondaryTapDownPosition, ) { return _CupertinoTextSelectionControlsToolbar( clipboardStatus: clipboardStatus, endpoints: endpoints, globalEditableRegion: globalEditableRegion, - handleCut: canCut(delegate) ? () => handleCut(delegate) : null, - handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null, + handleCut: canCut(delegate) ? () => handleCut(delegate, clipboardStatus) : null, + handleCopy: canCopy(delegate) ? () => handleCopy(delegate, clipboardStatus) : null, handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null, handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null, selectionMidpoint: selectionMidpoint, diff --git a/packages/flutter/lib/src/material/desktop_text_selection.dart b/packages/flutter/lib/src/material/desktop_text_selection.dart index ba8513d4e41b4..0ac7c22d88619 100644 --- a/packages/flutter/lib/src/material/desktop_text_selection.dart +++ b/packages/flutter/lib/src/material/desktop_text_selection.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -35,15 +34,15 @@ class _DesktopTextSelectionControls extends TextSelectionControls { Offset selectionMidpoint, List endpoints, TextSelectionDelegate delegate, - ValueListenable? clipboardStatus, + ClipboardStatusNotifier clipboardStatus, Offset? lastSecondaryTapDownPosition, ) { return _DesktopTextSelectionControlsToolbar( clipboardStatus: clipboardStatus, endpoints: endpoints, globalEditableRegion: globalEditableRegion, - handleCut: canCut(delegate) ? () => handleCut(delegate) : null, - handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null, + handleCut: canCut(delegate) ? () => handleCut(delegate, clipboardStatus) : null, + handleCopy: canCopy(delegate) ? () => handleCopy(delegate, clipboardStatus) : null, handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null, handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null, selectionMidpoint: selectionMidpoint, @@ -95,7 +94,7 @@ class _DesktopTextSelectionControlsToolbar extends StatefulWidget { required this.lastSecondaryTapDownPosition, }) : super(key: key); - final ValueListenable? clipboardStatus; + final ClipboardStatusNotifier? clipboardStatus; final List endpoints; final Rect globalEditableRegion; final VoidCallback? handleCopy; @@ -111,6 +110,8 @@ class _DesktopTextSelectionControlsToolbar extends StatefulWidget { } class _DesktopTextSelectionControlsToolbarState extends State<_DesktopTextSelectionControlsToolbar> { + ClipboardStatusNotifier? _clipboardStatus; + void _onChangedClipboardStatus() { setState(() { // Inform the widget that the value of clipboardStatus has changed. @@ -120,28 +121,46 @@ class _DesktopTextSelectionControlsToolbarState extends State<_DesktopTextSelect @override void initState() { super.initState(); - widget.clipboardStatus?.addListener(_onChangedClipboardStatus); + if (widget.handlePaste != null) { + _clipboardStatus = widget.clipboardStatus ?? ClipboardStatusNotifier(); + _clipboardStatus!.addListener(_onChangedClipboardStatus); + _clipboardStatus!.update(); + } } @override void didUpdateWidget(_DesktopTextSelectionControlsToolbar oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.clipboardStatus != widget.clipboardStatus) { - oldWidget.clipboardStatus?.removeListener(_onChangedClipboardStatus); - widget.clipboardStatus?.addListener(_onChangedClipboardStatus); + if (_clipboardStatus != null) { + _clipboardStatus!.removeListener(_onChangedClipboardStatus); + _clipboardStatus!.dispose(); + } + _clipboardStatus = widget.clipboardStatus ?? ClipboardStatusNotifier(); + _clipboardStatus!.addListener(_onChangedClipboardStatus); + if (widget.handlePaste != null) { + _clipboardStatus!.update(); + } } } @override void dispose() { super.dispose(); - widget.clipboardStatus?.removeListener(_onChangedClipboardStatus); + // When used in an Overlay, this can be disposed after its creator has + // already disposed _clipboardStatus. + if (_clipboardStatus != null && !_clipboardStatus!.disposed) { + _clipboardStatus!.removeListener(_onChangedClipboardStatus); + if (widget.clipboardStatus == null) { + _clipboardStatus!.dispose(); + } + } } @override Widget build(BuildContext context) { // Don't render the menu until the state of the clipboard is known. - if (widget.handlePaste != null && widget.clipboardStatus?.value == ClipboardStatus.unknown) { + if (widget.handlePaste != null && _clipboardStatus!.value == ClipboardStatus.unknown) { return const SizedBox(width: 0.0, height: 0.0); } @@ -178,7 +197,7 @@ class _DesktopTextSelectionControlsToolbarState extends State<_DesktopTextSelect addToolbarButton(localizations.copyButtonLabel, widget.handleCopy!); } if (widget.handlePaste != null - && widget.clipboardStatus?.value == ClipboardStatus.pasteable) { + && _clipboardStatus!.value == ClipboardStatus.pasteable) { addToolbarButton(localizations.pasteButtonLabel, widget.handlePaste!); } if (widget.handleSelectAll != null) { diff --git a/packages/flutter/lib/src/material/text_selection.dart b/packages/flutter/lib/src/material/text_selection.dart index 93e19363cb2fa..1af3e887eae19 100644 --- a/packages/flutter/lib/src/material/text_selection.dart +++ b/packages/flutter/lib/src/material/text_selection.dart @@ -4,7 +4,6 @@ import 'dart:math' as math; -import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -36,7 +35,7 @@ class MaterialTextSelectionControls extends TextSelectionControls { Offset selectionMidpoint, List endpoints, TextSelectionDelegate delegate, - ValueListenable? clipboardStatus, + ClipboardStatusNotifier clipboardStatus, Offset? lastSecondaryTapDownPosition, ) { return _TextSelectionControlsToolbar( @@ -46,8 +45,8 @@ class MaterialTextSelectionControls extends TextSelectionControls { endpoints: endpoints, delegate: delegate, clipboardStatus: clipboardStatus, - handleCut: canCut(delegate) ? () => handleCut(delegate) : null, - handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null, + handleCut: canCut(delegate) ? () => handleCut(delegate, clipboardStatus) : null, + handleCopy: canCopy(delegate) ? () => handleCopy(delegate, clipboardStatus) : null, handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null, handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null, ); @@ -144,7 +143,7 @@ class _TextSelectionControlsToolbar extends StatefulWidget { required this.textLineHeight, }) : super(key: key); - final ValueListenable? clipboardStatus; + final ClipboardStatusNotifier clipboardStatus; final TextSelectionDelegate delegate; final List endpoints; final Rect globalEditableRegion; @@ -169,22 +168,28 @@ class _TextSelectionControlsToolbarState extends State<_TextSelectionControlsToo @override void initState() { super.initState(); - widget.clipboardStatus?.addListener(_onChangedClipboardStatus); + widget.clipboardStatus.addListener(_onChangedClipboardStatus); + widget.clipboardStatus.update(); } @override void didUpdateWidget(_TextSelectionControlsToolbar oldWidget) { super.didUpdateWidget(oldWidget); if (widget.clipboardStatus != oldWidget.clipboardStatus) { - widget.clipboardStatus?.addListener(_onChangedClipboardStatus); - oldWidget.clipboardStatus?.removeListener(_onChangedClipboardStatus); + widget.clipboardStatus.addListener(_onChangedClipboardStatus); + oldWidget.clipboardStatus.removeListener(_onChangedClipboardStatus); } + widget.clipboardStatus.update(); } @override void dispose() { super.dispose(); - widget.clipboardStatus?.removeListener(_onChangedClipboardStatus); + // When used in an Overlay, it can happen that this is disposed after its + // creator has already disposed _clipboardStatus. + if (!widget.clipboardStatus.disposed) { + widget.clipboardStatus.removeListener(_onChangedClipboardStatus); + } } @override @@ -197,7 +202,7 @@ class _TextSelectionControlsToolbarState extends State<_TextSelectionControlsToo // If the paste button is desired, don't render anything until the state of // the clipboard is known, since it's used to determine if paste is shown. if (widget.handlePaste != null - && widget.clipboardStatus?.value == ClipboardStatus.unknown) { + && widget.clipboardStatus.value == ClipboardStatus.unknown) { return const SizedBox.shrink(); } @@ -233,7 +238,7 @@ class _TextSelectionControlsToolbarState extends State<_TextSelectionControlsToo onPressed: widget.handleCopy!, ), if (widget.handlePaste != null - && widget.clipboardStatus?.value == ClipboardStatus.pasteable) + && widget.clipboardStatus.value == ClipboardStatus.pasteable) _TextSelectionToolbarItemData( label: localizations.pasteButtonLabel, onPressed: widget.handlePaste!, diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index ed5f53351f2f2..0a6270c416fd3 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -1671,7 +1671,6 @@ class EditableTextState extends State with AutomaticKeepAliveClien break; } } - _clipboardStatus?.update(); } /// Cut current selection to [Clipboard]. @@ -1695,7 +1694,6 @@ class EditableTextState extends State with AutomaticKeepAliveClien }); hideToolbar(); } - _clipboardStatus?.update(); } /// Paste text from [Clipboard]. @@ -2921,7 +2919,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien if (_selectionOverlay == null || _selectionOverlay!.toolbarIsVisible) { return false; } - _clipboardStatus?.update(); + _selectionOverlay!.showToolbar(); return true; } @@ -3028,7 +3026,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien && copyEnabled && _hasFocus && (controls?.canCopy(this) ?? false) - ? () => controls!.handleCopy(this) + ? () => controls!.handleCopy(this, _clipboardStatus) : null; } @@ -3037,7 +3035,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien && cutEnabled && _hasFocus && (controls?.canCut(this) ?? false) - ? () => controls!.handleCut(this) + ? () => controls!.handleCut(this, _clipboardStatus) : null; } @@ -3183,11 +3181,20 @@ class EditableTextState extends State with AutomaticKeepAliveClien bringIntoView(newSelection.extent); } + Object? _hideToolbarIfVisible(DismissIntent intent) { + if (_selectionOverlay?.toolbarIsVisible ?? false) { + hideToolbar(false); + return null; + } + return Actions.invoke(context, intent); + } + late final Map> _actions = >{ DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false), ReplaceTextIntent: _replaceTextAction, UpdateSelectionIntent: _updateSelectionAction, DirectionalFocusIntent: DirectionalFocusAction.forTextField(), + DismissIntent: CallbackAction(onInvoke: _hideToolbarIfVisible), // Delete DeleteCharacterIntent: _makeOverridable(_DeleteTextAction(this, _characterBoundary)), @@ -3286,10 +3293,6 @@ class EditableTextState extends State with AutomaticKeepAliveClien textWidthBasis: widget.textWidthBasis, obscuringCharacter: widget.obscuringCharacter, obscureText: widget.obscureText, - autocorrect: widget.autocorrect, - smartDashesType: widget.smartDashesType, - smartQuotesType: widget.smartQuotesType, - enableSuggestions: widget.enableSuggestions, offset: offset, onCaretChanged: _handleCaretChanged, rendererIgnoresPointer: widget.rendererIgnoresPointer, @@ -3392,10 +3395,6 @@ class _Editable extends MultiChildRenderObjectWidget { this.locale, required this.obscuringCharacter, required this.obscureText, - required this.autocorrect, - required this.smartDashesType, - required this.smartQuotesType, - required this.enableSuggestions, required this.offset, this.onCaretChanged, this.rendererIgnoresPointer = false, @@ -3452,10 +3451,6 @@ class _Editable extends MultiChildRenderObjectWidget { final bool obscureText; final TextHeightBehavior? textHeightBehavior; final TextWidthBasis textWidthBasis; - final bool autocorrect; - final SmartDashesType smartDashesType; - final SmartQuotesType smartQuotesType; - final bool enableSuggestions; final ViewportOffset offset; final CaretChangedHandler? onCaretChanged; final bool rendererIgnoresPointer; diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index a6f29134c4a93..cb5c5dee03f0d 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -13,6 +13,7 @@ import 'binding.dart'; import 'debug.dart'; import 'focus_manager.dart'; import 'inherited_model.dart'; +import 'notification_listener.dart'; export 'package:flutter/foundation.dart' show factory, @@ -2390,6 +2391,13 @@ abstract class BuildContext { /// data down to them. void visitChildElements(ElementVisitor visitor); + /// Start bubbling this notification at the given build context. + /// + /// The notification will be delivered to any [NotificationListener] widgets + /// with the appropriate type parameters that are ancestors of the given + /// [BuildContext]. + void dispatchNotification(Notification notification); + /// Returns a description of the [Element] associated with the current build context. /// /// The `name` is typically something like "The element being rebuilt was". @@ -3101,6 +3109,39 @@ class BuildOwner { } } +/// Mixin this class to allow receiving [Notification] objects dispatched by +/// child elements. +/// +/// See also: +/// * [NotificationListener], for a widget that allows consuming notifications. +mixin NotifiableElementMixin on Element { + /// Called when a notification of the appropriate type arrives at this + /// location in the tree. + /// + /// Return true to cancel the notification bubbling. Return false to + /// allow the notification to continue to be dispatched to further ancestors. + bool onNotification(Notification notification); + + @override + void attachNotificationTree() { + _notificationTree = _NotificationNode(_parent?._notificationTree, this); + } +} + +class _NotificationNode { + _NotificationNode(this.parent, this.current); + + NotifiableElementMixin? current; + _NotificationNode? parent; + + void dispatchNotification(Notification notification) { + if (current?.onNotification(notification) ?? true) { + return; + } + parent?.dispatchNotification(notification); + } +} + /// An instantiation of a [Widget] at a particular location in the tree. /// /// Widgets describe how to configure a subtree but the same widget can be used @@ -3161,6 +3202,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { Element? _parent; DebugReassembleConfig? _debugReassembleConfig; + _NotificationNode? _notificationTree; /// Compare two widgets for equality. /// @@ -3614,6 +3656,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { owner!._registerGlobalKey(key, this); } _updateInheritance(); + attachNotificationTree(); } void _debugRemoveGlobalKeyReservation(Element child) { @@ -3950,6 +3993,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { _dependencies?.clear(); _hadUnsatisfiedDependencies = false; _updateInheritance(); + attachNotificationTree(); if (_dirty) owner!.scheduleBuildFor(this); if (hadDependencies) @@ -4231,6 +4275,20 @@ abstract class Element extends DiagnosticableTree implements BuildContext { return ancestor; } + /// Called in [Element.mount] and [Element.activate] to register this element in + /// the notification tree. + /// + /// This method is only exposed so that [NotifiableElementMixin] can be implemented. + /// Subclasses of [Element] that wish to respond to notifications should mix that + /// in instead. + /// + /// See also: + /// * [NotificationListener], a widget that allows listening to notifications. + @protected + void attachNotificationTree() { + _notificationTree = _parent?._notificationTree; + } + void _updateInheritance() { assert(_lifecycleState == _ElementLifecycle.active); _inheritedLookup = _parent?._inheritedLookup; @@ -4359,6 +4417,11 @@ abstract class Element extends DiagnosticableTree implements BuildContext { return chain; } + @override + void dispatchNotification(Notification notification) { + _notificationTree?.dispatchNotification(notification); + } + /// A short, textual description of this element. @override String toStringShort() => _widget?.toStringShort() ?? '${describeIdentity(this)}(DEFUNCT)'; diff --git a/packages/flutter/lib/src/widgets/notification_listener.dart b/packages/flutter/lib/src/widgets/notification_listener.dart index 8fbd75ad41649..fe98047746912 100644 --- a/packages/flutter/lib/src/widgets/notification_listener.dart +++ b/packages/flutter/lib/src/widgets/notification_listener.dart @@ -50,27 +50,6 @@ abstract class Notification { /// const constructors so that they can be used in const expressions. const Notification(); - /// Applied to each ancestor of the [dispatch] target. - /// - /// The [Notification] class implementation of this method dispatches the - /// given [Notification] to each ancestor [NotificationListener] widget. - /// - /// Subclasses can override this to apply additional filtering or to update - /// the notification as it is bubbled (for example, increasing a `depth` field - /// for each ancestor of a particular type). - @protected - @mustCallSuper - bool visitAncestor(Element element) { - if (element is StatelessElement) { - final Widget widget = element.widget; - if (widget is NotificationListener) { - if (widget._dispatch(this, element)) // that function checks the type dynamically - return false; - } - } - return true; - } - /// Start bubbling this notification at the given build context. /// /// The notification will be delivered to any [NotificationListener] widgets @@ -78,9 +57,7 @@ abstract class Notification { /// [BuildContext]. If the [BuildContext] is null, the notification is not /// dispatched. void dispatch(BuildContext? target) { - // The `target` may be null if the subtree the notification is supposed to be - // dispatched in is in the process of being disposed. - target?.visitAncestorElements(visitAncestor); + target?.dispatchNotification(this); } @override @@ -112,20 +89,13 @@ abstract class Notification { /// [runtimeType] is a subtype of `T`. /// /// To dispatch notifications, use the [Notification.dispatch] method. -class NotificationListener extends StatelessWidget { +class NotificationListener extends ProxyWidget { /// Creates a widget that listens for notifications. const NotificationListener({ Key? key, - required this.child, + required Widget child, this.onNotification, - }) : super(key: key); - - /// The widget directly below this widget in the tree. - /// - /// This is not necessarily the widget that dispatched the notification. - /// - /// {@macro flutter.widgets.ProxyWidget.child} - final Widget child; + }) : super(key: key, child: child); /// Called when a notification of the appropriate type arrives at this /// location in the tree. @@ -133,9 +103,6 @@ class NotificationListener extends StatelessWidget { /// Return true to cancel the notification bubbling. Return false to /// allow the notification to continue to be dispatched to further ancestors. /// - /// The notification's [Notification.visitAncestor] method is called for each - /// ancestor, and invokes this callback as appropriate. - /// /// Notifications vary in terms of when they are dispatched. There are two /// main possibilities: dispatch between frames, and dispatch during layout. /// @@ -146,16 +113,29 @@ class NotificationListener extends StatelessWidget { /// widgets that depend on layout, consider a [LayoutBuilder] instead. final NotificationListenerCallback? onNotification; - bool _dispatch(Notification notification, Element element) { - if (onNotification != null && notification is T) { - final bool result = onNotification!(notification); - return result == true; // so that null and false have the same effect + @override + Element createElement() { + return _NotificationElement(this); + } +} + +/// An element used to host [NotificationListener] elements. +class _NotificationElement extends ProxyElement with NotifiableElementMixin { + _NotificationElement(NotificationListener widget) : super(widget); + + @override + bool onNotification(Notification notification) { + final NotificationListener listener = widget as NotificationListener; + if (listener.onNotification != null && notification is T) { + return listener.onNotification!(notification); } return false; } @override - Widget build(BuildContext context) => child; + void notifyClients(covariant ProxyWidget oldWidget) { + // Notification tree does not need to notify clients. + } } /// Indicates that the layout of one of the descendants of the object receiving @@ -186,4 +166,7 @@ class NotificationListener extends StatelessWidget { /// useful for paint effects that depend on the layout. If you were to use this /// notification to change the build, for instance, you would always be one /// frame behind, which would look really ugly and laggy. -class LayoutChangedNotification extends Notification { } +class LayoutChangedNotification extends Notification { + /// Create a new [LayoutChangedNotification]. + const LayoutChangedNotification(); +} diff --git a/packages/flutter/lib/src/widgets/scroll_notification.dart b/packages/flutter/lib/src/widgets/scroll_notification.dart index f0a2bfb0b5f8c..797ebe7240c42 100644 --- a/packages/flutter/lib/src/widgets/scroll_notification.dart +++ b/packages/flutter/lib/src/widgets/scroll_notification.dart @@ -24,13 +24,6 @@ mixin ViewportNotificationMixin on Notification { int get depth => _depth; int _depth = 0; - @override - bool visitAncestor(Element element) { - if (element is RenderObjectElement && element.renderObject is RenderAbstractViewport) - _depth += 1; - return super.visitAncestor(element); - } - @override void debugFillDescription(List description) { super.debugFillDescription(description); @@ -38,6 +31,22 @@ mixin ViewportNotificationMixin on Notification { } } +/// A mixin that allows [Element]s containing [Viewport] like widgets to correctly +/// modify the notification depth of a [ViewportNotificationMixin]. +/// +/// See also: +/// * [Viewport], which creates a custom [MultiChildRenderObjectElement] that mixes +/// this in. +mixin ViewportElementMixin on NotifiableElementMixin { + @override + bool onNotification(Notification notification) { + if (notification is ViewportNotificationMixin) { + notification._depth += 1; + } + return false; + } +} + /// A [Notification] related to scrolling. /// /// [Scrollable] widgets notify their ancestors about scrolling-related changes. diff --git a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart index ad210e88c86af..a06a0c1276668 100644 --- a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart @@ -318,6 +318,15 @@ class _SingleChildViewport extends SingleChildRenderObjectWidget { ..offset = offset ..clipBehavior = clipBehavior; } + + @override + SingleChildRenderObjectElement createElement() { + return _SingleChildViewportElement(this); + } +} + +class _SingleChildViewportElement extends SingleChildRenderObjectElement with NotifiableElementMixin, ViewportElementMixin { + _SingleChildViewportElement(_SingleChildViewport widget) : super(widget); } class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMixin implements RenderAbstractViewport { diff --git a/packages/flutter/lib/src/widgets/size_changed_layout_notifier.dart b/packages/flutter/lib/src/widgets/size_changed_layout_notifier.dart index 42c4495cd4a6f..c53f65946a218 100644 --- a/packages/flutter/lib/src/widgets/size_changed_layout_notifier.dart +++ b/packages/flutter/lib/src/widgets/size_changed_layout_notifier.dart @@ -28,7 +28,10 @@ import 'notification_listener.dart'; /// /// * [SizeChangedLayoutNotifier], which sends this notification. /// * [LayoutChangedNotification], of which this is a subclass. -class SizeChangedLayoutNotification extends LayoutChangedNotification { } +class SizeChangedLayoutNotification extends LayoutChangedNotification { + /// Create a new [SizeChangedLayoutNotification]. + const SizeChangedLayoutNotification(); +} /// A widget that automatically dispatches a [SizeChangedLayoutNotification] /// when the layout dimensions of its child change. @@ -61,7 +64,7 @@ class SizeChangedLayoutNotifier extends SingleChildRenderObjectWidget { RenderObject createRenderObject(BuildContext context) { return _RenderSizeChangedWithCallback( onLayoutChangedCallback: () { - SizeChangedLayoutNotification().dispatch(context); + const SizeChangedLayoutNotification().dispatch(context); }, ); } diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index 9745ee635d92e..2baed764a141a 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -142,7 +142,7 @@ abstract class TextSelectionControls { Offset position, List endpoints, TextSelectionDelegate delegate, - ValueListenable? clipboardStatus, + ClipboardStatusNotifier clipboardStatus, Offset? lastSecondaryTapDownPosition, ); @@ -199,16 +199,18 @@ abstract class TextSelectionControls { /// /// This is called by subclasses when their cut affordance is activated by /// the user. - void handleCut(TextSelectionDelegate delegate) { + void handleCut(TextSelectionDelegate delegate, ClipboardStatusNotifier? clipboardStatus) { delegate.cutSelection(SelectionChangedCause.toolbar); + clipboardStatus?.update(); } /// Call [TextSelectionDelegate.copySelection] to copy current selection. /// /// This is called by subclasses when their copy affordance is activated by /// the user. - void handleCopy(TextSelectionDelegate delegate) { + void handleCopy(TextSelectionDelegate delegate, ClipboardStatusNotifier? clipboardStatus) { delegate.copySelection(SelectionChangedCause.toolbar); + clipboardStatus?.update(); } /// Call [TextSelectionDelegate.pasteText] to paste text. @@ -1078,7 +1080,7 @@ class _SelectionToolbarOverlayState extends State<_SelectionToolbarOverlay> with widget.midpoint, widget.selectionEndpoints, widget.selectionDelegate!, - widget.clipboardStatus, + widget.clipboardStatus!, widget.toolbarLocation, ); }, @@ -2129,6 +2131,8 @@ class ClipboardStatusNotifier extends ValueNotifier with Widget }) : super(value); bool _disposed = false; + /// True if this instance has been disposed. + bool get disposed => _disposed; /// Check the [Clipboard] and update [value] if needed. Future update() async { @@ -2179,7 +2183,7 @@ class ClipboardStatusNotifier extends ValueNotifier with Widget @override void removeListener(VoidCallback listener) { super.removeListener(listener); - if (!_disposed && !hasListeners) { + if (!hasListeners) { WidgetsBinding.instance.removeObserver(this); } } @@ -2199,9 +2203,9 @@ class ClipboardStatusNotifier extends ValueNotifier with Widget @override void dispose() { + super.dispose(); WidgetsBinding.instance.removeObserver(this); _disposed = true; - super.dispose(); } } diff --git a/packages/flutter/lib/src/widgets/viewport.dart b/packages/flutter/lib/src/widgets/viewport.dart index 879784ae7271d..a7ab8a9797cf7 100644 --- a/packages/flutter/lib/src/widgets/viewport.dart +++ b/packages/flutter/lib/src/widgets/viewport.dart @@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart'; import 'basic.dart'; import 'debug.dart'; import 'framework.dart'; +import 'scroll_notification.dart'; export 'package:flutter/rendering.dart' show AxisDirection, @@ -43,6 +44,8 @@ export 'package:flutter/rendering.dart' show /// sliver context (the opposite of this widget). /// * [ShrinkWrappingViewport], a variant of [Viewport] that shrink-wraps its /// contents along the main axis. +/// * [ViewportElementMixin], which should be mixed in to the [Element] type used +/// by viewport-like widgets to correctly handle scroll notifications. class Viewport extends MultiChildRenderObjectWidget { /// Creates a widget that is bigger on the inside. /// @@ -207,7 +210,7 @@ class Viewport extends MultiChildRenderObjectWidget { } } -class _ViewportElement extends MultiChildRenderObjectElement { +class _ViewportElement extends MultiChildRenderObjectElement with NotifiableElementMixin, ViewportElementMixin { /// Creates an element that uses the given widget as its configuration. _ViewportElement(Viewport widget) : super(widget); diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index 8f98a90b66558..88d153e22b09c 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -43,7 +43,7 @@ class MockTextSelectionControls extends TextSelectionControls { Offset position, List endpoints, TextSelectionDelegate delegate, - ValueListenable? clipboardStatus, + ClipboardStatusNotifier clipboardStatus, Offset? lastSecondaryTapDownPosition, ) { throw UnimplementedError(); diff --git a/packages/flutter/test/cupertino/text_selection_toolbar_test.dart b/packages/flutter/test/cupertino/text_selection_toolbar_test.dart index 4474026425519..7156dd1117ab4 100644 --- a/packages/flutter/test/cupertino/text_selection_toolbar_test.dart +++ b/packages/flutter/test/cupertino/text_selection_toolbar_test.dart @@ -24,7 +24,7 @@ class _CustomCupertinoTextSelectionControls extends CupertinoTextSelectionContro Offset selectionMidpoint, List endpoints, TextSelectionDelegate delegate, - ValueListenable? clipboardStatus, + ClipboardStatusNotifier clipboardStatus, Offset? lastSecondaryTapDownPosition, ) { final MediaQueryData mediaQuery = MediaQuery.of(context); diff --git a/packages/flutter/test/material/material_test.dart b/packages/flutter/test/material/material_test.dart index b50431b5de598..7b85c12a70951 100644 --- a/packages/flutter/test/material/material_test.dart +++ b/packages/flutter/test/material/material_test.dart @@ -17,7 +17,7 @@ class NotifyMaterial extends StatelessWidget { const NotifyMaterial({ Key? key }) : super(key: key); @override Widget build(BuildContext context) { - LayoutChangedNotification().dispatch(context); + const LayoutChangedNotification().dispatch(context); return Container(); } } diff --git a/packages/flutter/test/material/text_selection_toolbar_test.dart b/packages/flutter/test/material/text_selection_toolbar_test.dart index 2b67ef5e6a17c..eaea86f947e16 100644 --- a/packages/flutter/test/material/text_selection_toolbar_test.dart +++ b/packages/flutter/test/material/text_selection_toolbar_test.dart @@ -22,7 +22,7 @@ class _CustomMaterialTextSelectionControls extends MaterialTextSelectionControls Offset selectionMidpoint, List endpoints, TextSelectionDelegate delegate, - ValueListenable? clipboardStatus, + ClipboardStatusNotifier clipboardStatus, Offset? lastSecondaryTapDownPosition, ) { final TextSelectionPoint startTextSelectionPoint = endpoints[0]; diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 497b1d4f2cbe0..eff24bb403572 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -1318,6 +1318,41 @@ void main() { expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget); }); + testWidgets('can hide toolbar with DismissIntent', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: EditableText( + backgroundCursorColor: Colors.grey, + controller: controller, + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + selectionControls: materialTextSelectionControls, + ), + ), + ); + + final EditableTextState state = + tester.state(find.byType(EditableText)); + + // Show the toolbar + state.renderEditable.selectWordsInRange( + from: Offset.zero, + cause: SelectionChangedCause.tap, + ); + await tester.pump(); + + // On web, we don't let Flutter show the toolbar. + expect(state.showToolbar(), kIsWeb ? isFalse : isTrue); + await tester.pumpAndSettle(); + expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget); + + // Hide the menu using the DismissIntent. + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pump(); + expect(find.text('Paste'), findsNothing); + }); + testWidgets('Paste is shown only when there is something to paste', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -11747,7 +11782,7 @@ class MockTextFormatter extends TextInputFormatter { class MockTextSelectionControls extends Fake implements TextSelectionControls { @override - Widget buildToolbar(BuildContext context, Rect globalEditableRegion, double textLineHeight, Offset position, List endpoints, TextSelectionDelegate delegate, ValueListenable? clipboardStatus, Offset? lastSecondaryTapDownPosition) { + Widget buildToolbar(BuildContext context, Rect globalEditableRegion, double textLineHeight, Offset position, List endpoints, TextSelectionDelegate delegate, ClipboardStatusNotifier clipboardStatus, Offset? lastSecondaryTapDownPosition) { return Container(); } @@ -11775,7 +11810,7 @@ class MockTextSelectionControls extends Fake implements TextSelectionControls { int copyCount = 0; @override - void handleCopy(TextSelectionDelegate delegate) { + void handleCopy(TextSelectionDelegate delegate, ClipboardStatusNotifier? clipboardStatus) { copyCount += 1; } @@ -11785,7 +11820,7 @@ class MockTextSelectionControls extends Fake implements TextSelectionControls { } @override - void handleCut(TextSelectionDelegate delegate) { + void handleCut(TextSelectionDelegate delegate, ClipboardStatusNotifier? clipboardStatus) { cutCount += 1; } diff --git a/packages/flutter/test/widgets/text_selection_test.dart b/packages/flutter/test/widgets/text_selection_test.dart index f5232d3803b53..e6ba90170f365 100644 --- a/packages/flutter/test/widgets/text_selection_test.dart +++ b/packages/flutter/test/widgets/text_selection_test.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart' show defaultTargetPlatform, ValueListenable; +import 'package:flutter/foundation.dart' show defaultTargetPlatform; import 'package:flutter/gestures.dart' show PointerDeviceKind, kSecondaryButton; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -1034,6 +1034,28 @@ void main() { }); }); }); + + group('TextSelectionControls', () { + test('ClipboardStatusNotifier is updated on handleCut', () async { + final FakeClipboardStatusNotifier clipboardStatus = FakeClipboardStatusNotifier(); + final FakeTextSelectionDelegate delegate = FakeTextSelectionDelegate(); + final CustomTextSelectionControls textSelectionControls = CustomTextSelectionControls(); + + expect(clipboardStatus.updateCalled, false); + textSelectionControls.handleCut(delegate, clipboardStatus); + expect(clipboardStatus.updateCalled, true); + }); + + test('ClipboardStatusNotifier is updated on handleCopy', () async { + final FakeClipboardStatusNotifier clipboardStatus = FakeClipboardStatusNotifier(); + final FakeTextSelectionDelegate delegate = FakeTextSelectionDelegate(); + final CustomTextSelectionControls textSelectionControls = CustomTextSelectionControls(); + + expect(clipboardStatus.updateCalled, false); + textSelectionControls.handleCopy(delegate, clipboardStatus); + expect(clipboardStatus.updateCalled, true); + }); + }); } class FakeTextSelectionGestureDetectorBuilderDelegate implements TextSelectionGestureDetectorBuilderDelegate { @@ -1162,7 +1184,7 @@ class CustomTextSelectionControls extends TextSelectionControls { Offset position, List endpoints, TextSelectionDelegate delegate, - ValueListenable? clipboardStatus, + ClipboardStatusNotifier clipboardStatus, Offset? lastSecondaryTapDownPosition, ) { throw UnimplementedError(); @@ -1199,15 +1221,15 @@ class TextSelectionControlsSpy extends TextSelectionControls { @override Widget buildToolbar( - BuildContext context, - Rect globalEditableRegion, - double textLineHeight, - Offset position, - List endpoints, - TextSelectionDelegate delegate, - ValueListenable? clipboardStatus, - Offset? lastSecondaryTapDownPosition, - ) { + BuildContext context, + Rect globalEditableRegion, + double textLineHeight, + Offset position, + List endpoints, + TextSelectionDelegate delegate, + ClipboardStatusNotifier clipboardStatus, + Offset? lastSecondaryTapDownPosition, + ) { return Text('dummy', key: toolBarKey); } @@ -1225,6 +1247,9 @@ class TextSelectionControlsSpy extends TextSelectionControls { class FakeClipboardStatusNotifier extends ClipboardStatusNotifier { FakeClipboardStatusNotifier() : super(value: ClipboardStatus.unknown); + @override + bool get disposed => false; + bool updateCalled = false; @override Future update() async { diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart index 362f4ccede6b7..a8af06c48734e 100644 --- a/packages/flutter_tools/lib/src/base/user_messages.dart +++ b/packages/flutter_tools/lib/src/base/user_messages.dart @@ -147,7 +147,7 @@ class UserMessages { String xcodeLocation(String location) => 'Xcode at $location'; String xcodeOutdated(String requiredVersion) => - 'Flutter requires a minimum Xcode version of $requiredVersion.\n' + 'Flutter requires Xcode $requiredVersion or higher.\n' 'Download the latest version or update via the Mac App Store.'; String xcodeRecommended(String recommendedVersion) => diff --git a/packages/flutter_tools/lib/src/build_system/targets/dart_plugin_registrant.dart b/packages/flutter_tools/lib/src/build_system/targets/dart_plugin_registrant.dart index c3054f6be6edb..e18e767854cb9 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/dart_plugin_registrant.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/dart_plugin_registrant.dart @@ -12,7 +12,7 @@ import '../../flutter_plugins.dart'; import '../../project.dart'; import '../build_system.dart'; -/// Generates a new `./dart_tool/flutter_build/generated_main.dart` +/// Generates a new `./dart_tool/flutter_build/dart_plugin_registrant.dart` /// based on the current dependency map in `pubspec.lock`. class DartPluginRegistrantTarget extends Target { /// Construct a [DartPluginRegistrantTarget]. @@ -86,7 +86,7 @@ class DartPluginRegistrantTarget extends Target { @override List get outputs => [ const Source.pattern( - '{PROJECT_DIR}/.dart_tool/flutter_build/generated_main.dart', + '{PROJECT_DIR}/.dart_tool/flutter_build/dart_plugin_registrant.dart', optional: true, ), ]; diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart index 04ac530e89851..139e62780bb1f 100644 --- a/packages/flutter_tools/lib/src/compile.dart +++ b/packages/flutter_tools/lib/src/compile.dart @@ -261,14 +261,12 @@ class KernelCompiler { if (outputFilePath != null && !_fileSystem.isFileSync(outputFilePath)) { _fileSystem.file(outputFilePath).createSync(recursive: true); } - if (buildDir != null && checkDartPluginRegistry) { - // Check if there's a Dart plugin registrant. - // This is contained in the file `generated_main.dart` under `.dart_tools/flutter_build/`. - final File newMainDart = buildDir.parent.childFile('generated_main.dart'); - if (newMainDart.existsSync()) { - mainUri = newMainDart.path; - } - } + + // Check if there's a Dart plugin registrant. + // This is contained in the file `dart_plugin_registrant.dart` under `.dart_tools/flutter_build/`. + final File? dartPluginRegistrant = checkDartPluginRegistry + ? buildDir?.parent.childFile('dart_plugin_registrant.dart') + : null; final List command = [ engineDartPath, @@ -316,6 +314,10 @@ class KernelCompiler { '--platform', platformDill, ], + if (dartPluginRegistrant != null && dartPluginRegistrant.existsSync()) ...[ + '--source', + dartPluginRegistrant.path, + ], ...?extraFrontEndOptions, mainUri, ]; @@ -359,6 +361,7 @@ class _RecompileRequest extends _CompilationRequest { this.outputPath, this.packageConfig, this.suppressErrors, + {this.additionalSource} ) : super(completer); Uri mainUri; @@ -366,6 +369,7 @@ class _RecompileRequest extends _CompilationRequest { String outputPath; PackageConfig packageConfig; bool suppressErrors; + final String? additionalSource; @override Future _run(DefaultResidentCompiler compiler) async => @@ -628,24 +632,31 @@ class DefaultResidentCompiler implements ResidentCompiler { if (!_controller.hasListener) { _controller.stream.listen(_handleCompilationRequest); } - // `generated_main.dart` contains the Dart plugin registry. + String? additionalSource; + // `dart_plugin_registrant.dart` contains the Dart plugin registry. if (checkDartPluginRegistry && projectRootPath != null && fs != null) { - final File generatedMainDart = fs.file( + final File dartPluginRegistrantDart = fs.file( fs.path.join( projectRootPath, '.dart_tool', 'flutter_build', - 'generated_main.dart', + 'dart_plugin_registrant.dart', ), ); - if (generatedMainDart != null && generatedMainDart.existsSync()) { - mainUri = generatedMainDart.uri; + if (dartPluginRegistrantDart != null && dartPluginRegistrantDart.existsSync()) { + additionalSource = dartPluginRegistrantDart.path; } } final Completer completer = Completer(); - _controller.add( - _RecompileRequest(completer, mainUri, invalidatedFiles, outputPath, packageConfig, suppressErrors) - ); + _controller.add(_RecompileRequest( + completer, + mainUri, + invalidatedFiles, + outputPath, + packageConfig, + suppressErrors, + additionalSource: additionalSource, + )); return completer.future; } @@ -659,7 +670,7 @@ class DefaultResidentCompiler implements ResidentCompiler { final Process? server = _server; if (server == null) { - return _compile(mainUri, request.outputPath); + return _compile(mainUri, request.outputPath, additionalSource: request.additionalSource); } final String inputKey = Uuid().generateV4(); @@ -705,6 +716,7 @@ class DefaultResidentCompiler implements ResidentCompiler { Future _compile( String scriptUri, String? outputPath, + {String? additionalSource} ) async { final String frontendServer = _artifacts.getArtifactPath( Artifact.frontendServerSnapshotForEngineDartSdk @@ -756,6 +768,10 @@ class DefaultResidentCompiler implements ResidentCompiler { '--initialize-from-dill', initializeFromDill!, ], + if (additionalSource != null) ...[ + '--source', + additionalSource, + ], if (platformDill != null) ...[ '--platform', platformDill!, diff --git a/packages/flutter_tools/lib/src/flutter_plugins.dart b/packages/flutter_tools/lib/src/flutter_plugins.dart index f9d949d7a1f2e..d6a7e088f205a 100644 --- a/packages/flutter_tools/lib/src/flutter_plugins.dart +++ b/packages/flutter_tools/lib/src/flutter_plugins.dart @@ -706,10 +706,6 @@ const String _dartPluginRegistryForNonWebTemplate = ''' // @dart = {{dartLanguageVersion}} -// When `{{mainEntrypoint}}` defines `main`, that definition is shadowed by the definition below. -export '{{mainEntrypoint}}'; - -import '{{mainEntrypoint}}' as entrypoint; import 'dart:io'; // flutter_ignore: dart_io_import. {{#android}} import 'package:{{pluginName}}/{{pluginName}}.dart'; @@ -754,18 +750,6 @@ $_dartPluginRegisterWith {{/windows}} } } - -} - -typedef _UnaryFunction = dynamic Function(List args); -typedef _NullaryFunction = dynamic Function(); - -void main(List args) { - if (entrypoint.main is _UnaryFunction) { - (entrypoint.main as _UnaryFunction)(args); - } else { - (entrypoint.main as _NullaryFunction)(); - } } '''; diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart index 322ff318c91f1..1096e0eb9e61f 100644 --- a/packages/flutter_tools/lib/src/macos/xcode.dart +++ b/packages/flutter_tools/lib/src/macos/xcode.dart @@ -18,11 +18,11 @@ import '../base/version.dart'; import '../build_info.dart'; import '../ios/xcodeproj.dart'; -Version get xcodeRequiredVersion => Version(12, 3, null, text: '12.3'); +Version get xcodeRequiredVersion => Version(13, null, null); /// Diverging this number from the minimum required version will provide a doctor /// warning, not error, that users should upgrade Xcode. -Version get xcodeRecommendedVersion => Version(13, null, null, text: '13'); +Version get xcodeRecommendedVersion => xcodeRequiredVersion; /// SDK name passed to `xcrun --sdk`. Corresponds to undocumented Xcode /// SUPPORTED_PLATFORMS values. diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 3d2a23a9f95e6..52276c4aceff6 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -223,7 +223,7 @@ class FlutterProject { /// The generated Dart plugin registrant for non-web platforms. File get dartPluginRegistrant => dartTool .childDirectory('flutter_build') - .childFile('generated_main.dart'); + .childFile('dart_plugin_registrant.dart'); /// The example sub-project of this project. FlutterProject get example => FlutterProject( diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/dart_plugin_registrant_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/dart_plugin_registrant_test.dart index 64c628ded742b..6b97b1ed411b5 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/dart_plugin_registrant_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/dart_plugin_registrant_test.dart @@ -159,7 +159,7 @@ void main() { } }); - testUsingContext("doesn't generate generated_main.dart if there aren't Dart plugins", () async { + testUsingContext("doesn't generate dart_plugin_registrant.dart if there aren't Dart plugins", () async { final Directory projectDir = fileSystem.directory('project')..createSync(); final Environment environment = Environment.test( fileSystem.currentDirectory, @@ -189,11 +189,11 @@ void main() { final File generatedMain = projectDir .childDirectory('.dart_tool') .childDirectory('flutter_build') - .childFile('generated_main.dart'); + .childFile('dart_plugin_registrant.dart'); expect(generatedMain.existsSync(), isFalse); }); - testUsingContext('regenerates generated_main.dart', () async { + testUsingContext('regenerates dart_plugin_registrant.dart', () async { final Directory projectDir = fileSystem.directory('project')..createSync(); final Environment environment = Environment.test( fileSystem.currentDirectory, @@ -231,7 +231,7 @@ void main() { final File generatedMain = projectDir .childDirectory('.dart_tool') .childDirectory('flutter_build') - .childFile('generated_main.dart'); + .childFile('dart_plugin_registrant.dart'); final String mainContent = generatedMain.readAsStringSync(); expect( mainContent, @@ -243,10 +243,6 @@ void main() { '\n' '// @dart = 2.12\n' '\n' - '// When `package:path_provider_example/main.dart` defines `main`, that definition is shadowed by the definition below.\n' - "export 'package:path_provider_example/main.dart';\n" - '\n' - "import 'package:path_provider_example/main.dart' as entrypoint;\n" "import 'dart:io'; // flutter_ignore: dart_io_import.\n" "import 'package:path_provider_linux/path_provider_linux.dart';\n" '\n' @@ -272,24 +268,12 @@ void main() { ' } else if (Platform.isWindows) {\n' ' }\n' ' }\n' - '\n' - '}\n' - '\n' - 'typedef _UnaryFunction = dynamic Function(List args);\n' - 'typedef _NullaryFunction = dynamic Function();\n' - '\n' - 'void main(List args) {\n' - ' if (entrypoint.main is _UnaryFunction) {\n' - ' (entrypoint.main as _UnaryFunction)(args);\n' - ' } else {\n' - ' (entrypoint.main as _NullaryFunction)();\n' - ' }\n' '}\n' ), ); }); - testUsingContext('removes generated_main.dart if plugins are removed from pubspec.yaml', () async { + testUsingContext('removes dart_plugin_registrant.dart if plugins are removed from pubspec.yaml', () async { final Directory projectDir = fileSystem.directory('project')..createSync(); final Environment environment = Environment.test( fileSystem.currentDirectory, @@ -321,7 +305,7 @@ void main() { final File generatedMain = projectDir .childDirectory('.dart_tool') .childDirectory('flutter_build') - .childFile('generated_main.dart'); + .childFile('dart_plugin_registrant.dart'); final FlutterProject testProject = FlutterProject.fromDirectoryTest(projectDir); await DartPluginRegistrantTarget.test(testProject).build(environment); @@ -372,7 +356,7 @@ void main() { final File generatedMain = projectDir .childDirectory('.dart_tool') .childDirectory('flutter_build') - .childFile('generated_main.dart'); + .childFile('dart_plugin_registrant.dart'); final String mainContent = generatedMain.readAsStringSync(); expect( @@ -385,10 +369,6 @@ void main() { '\n' '// @dart = 2.12\n' '\n' - '// When `file:///root/external.dart` defines `main`, that definition is shadowed by the definition below.\n' - "export 'file:///root/external.dart';\n" - '\n' - "import 'file:///root/external.dart' as entrypoint;\n" "import 'dart:io'; // flutter_ignore: dart_io_import.\n" "import 'package:path_provider_linux/path_provider_linux.dart';\n" '\n' @@ -414,18 +394,6 @@ void main() { ' } else if (Platform.isWindows) {\n' ' }\n' ' }\n' - '\n' - '}\n' - '\n' - 'typedef _UnaryFunction = dynamic Function(List args);\n' - 'typedef _NullaryFunction = dynamic Function();\n' - '\n' - 'void main(List args) {\n' - ' if (entrypoint.main is _UnaryFunction) {\n' - ' (entrypoint.main as _UnaryFunction)(args);\n' - ' } else {\n' - ' (entrypoint.main as _NullaryFunction)();\n' - ' }\n' '}\n' ), ); diff --git a/packages/flutter_tools/test/general.shard/compile_batch_test.dart b/packages/flutter_tools/test/general.shard/compile_batch_test.dart index 627db081fa36e..519dd11993a56 100644 --- a/packages/flutter_tools/test/general.shard/compile_batch_test.dart +++ b/packages/flutter_tools/test/general.shard/compile_batch_test.dart @@ -395,7 +395,9 @@ void main() { '--no-link-platform', '--packages', '.packages', - '.dart_tools/flutter_build/generated_main.dart', + '--source', + '.dart_tools/flutter_build/dart_plugin_registrant.dart', + 'scheme:///main.dart', ], completer: completer), ]), stdoutHandler: stdoutHandler, @@ -405,7 +407,7 @@ void main() { .childDirectory('flutter_build') .childDirectory('test'); - buildDir.parent.childFile('generated_main.dart').createSync(recursive: true); + buildDir.parent.childFile('dart_plugin_registrant.dart').createSync(recursive: true); final Future output = kernelCompiler.compile(sdkRoot: '/path/to/sdkroot', mainPath: '/foo/bar/fizz/main.dart', diff --git a/packages/flutter_tools/test/general.shard/dart_plugin_test.dart b/packages/flutter_tools/test/general.shard/dart_plugin_test.dart index 2a7364f7bbccc..5cbea5fc0b927 100644 --- a/packages/flutter_tools/test/general.shard/dart_plugin_test.dart +++ b/packages/flutter_tools/test/general.shard/dart_plugin_test.dart @@ -35,7 +35,7 @@ void main() { ..directory = directory ..flutterPluginsFile = directory.childFile('.flutter-plugins') ..flutterPluginsDependenciesFile = directory.childFile('.flutter-plugins-dependencies') - ..dartPluginRegistrant = directory.childFile('generated_main.dart'); + ..dartPluginRegistrant = directory.childFile('dart_plugin_registrant.dart'); flutterProject.directory.childFile('.packages').createSync(recursive: true); }); @@ -701,206 +701,6 @@ void main() { '\n' '// @dart = 2.8\n' '\n' - '// When `package:app/main.dart` defines `main`, that definition is shadowed by the definition below.\n' - "export 'package:app/main.dart';\n" - '\n' - "import 'package:app/main.dart' as entrypoint;\n" - "import 'dart:io'; // flutter_ignore: dart_io_import.\n" - "import 'package:url_launcher_android/url_launcher_android.dart';\n" - "import 'package:url_launcher_ios/url_launcher_ios.dart';\n" - "import 'package:url_launcher_linux/url_launcher_linux.dart';\n" - "import 'package:awesome_macos/awesome_macos.dart';\n" - "import 'package:url_launcher_macos/url_launcher_macos.dart';\n" - "import 'package:url_launcher_windows/url_launcher_windows.dart';\n" - '\n' - "@pragma('vm:entry-point')\n" - 'class _PluginRegistrant {\n' - '\n' - " @pragma('vm:entry-point')\n" - ' static void register() {\n' - ' if (Platform.isAndroid) {\n' - ' try {\n' - ' AndroidPlugin.registerWith();\n' - ' } catch (err) {\n' - ' print(\n' - " '`url_launcher_android` threw an error: \$err. '\n" - " 'The app may not function as expected until you remove this plugin from pubspec.yaml'\n" - ' );\n' - ' rethrow;\n' - ' }\n' - '\n' - ' } else if (Platform.isIOS) {\n' - ' try {\n' - ' IosPlugin.registerWith();\n' - ' } catch (err) {\n' - ' print(\n' - " '`url_launcher_ios` threw an error: \$err. '\n" - " 'The app may not function as expected until you remove this plugin from pubspec.yaml'\n" - ' );\n' - ' rethrow;\n' - ' }\n' - '\n' - ' } else if (Platform.isLinux) {\n' - ' try {\n' - ' LinuxPlugin.registerWith();\n' - ' } catch (err) {\n' - ' print(\n' - " '`url_launcher_linux` threw an error: \$err. '\n" - " 'The app may not function as expected until you remove this plugin from pubspec.yaml'\n" - ' );\n' - ' rethrow;\n' - ' }\n' - '\n' - ' } else if (Platform.isMacOS) {\n' - ' try {\n' - ' AwesomeMacOS.registerWith();\n' - ' } catch (err) {\n' - ' print(\n' - " '`awesome_macos` threw an error: \$err. '\n" - " 'The app may not function as expected until you remove this plugin from pubspec.yaml'\n" - ' );\n' - ' rethrow;\n' - ' }\n' - '\n' - ' try {\n' - ' MacOSPlugin.registerWith();\n' - ' } catch (err) {\n' - ' print(\n' - " '`url_launcher_macos` threw an error: \$err. '\n" - " 'The app may not function as expected until you remove this plugin from pubspec.yaml'\n" - ' );\n' - ' rethrow;\n' - ' }\n' - '\n' - ' } else if (Platform.isWindows) {\n' - ' try {\n' - ' WindowsPlugin.registerWith();\n' - ' } catch (err) {\n' - ' print(\n' - " '`url_launcher_windows` threw an error: \$err. '\n" - " 'The app may not function as expected until you remove this plugin from pubspec.yaml'\n" - ' );\n' - ' rethrow;\n' - ' }\n' - '\n' - ' }\n' - ' }\n' - '\n' - '}\n' - '\n' - 'typedef _UnaryFunction = dynamic Function(List args);\n' - 'typedef _NullaryFunction = dynamic Function();\n' - '\n' - 'void main(List args) {\n' - ' if (entrypoint.main is _UnaryFunction) {\n' - ' (entrypoint.main as _UnaryFunction)(args);\n' - ' } else {\n' - ' (entrypoint.main as _NullaryFunction)();\n' - ' }\n' - '}\n', - ); - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('Rewires entrypoints', () async { - flutterProject.isModule = true; - - createFakeDartPlugins( - flutterProject, - flutterManifest, - fs, - { - 'url_launcher_android': ''' - flutter: - plugin: - implements: url_launcher - platforms: - android: - dartPluginClass: AndroidPlugin -''', - 'url_launcher_ios': ''' - flutter: - plugin: - implements: url_launcher - platforms: - ios: - dartPluginClass: IosPlugin -''', - 'url_launcher_macos': ''' - flutter: - plugin: - implements: url_launcher - platforms: - macos: - dartPluginClass: MacOSPlugin -''', - 'url_launcher_linux': ''' - flutter: - plugin: - implements: url_launcher - platforms: - linux: - dartPluginClass: LinuxPlugin -''', - 'url_launcher_windows': ''' - flutter: - plugin: - implements: url_launcher - platforms: - windows: - dartPluginClass: WindowsPlugin -''', - 'awesome_macos': ''' - flutter: - plugin: - implements: awesome - platforms: - macos: - dartPluginClass: AwesomeMacOS -''' - }); - - final Directory libDir = flutterProject.directory.childDirectory('lib'); - libDir.createSync(recursive: true); - - final File mainFile = libDir.childFile('main.dart'); - mainFile.writeAsStringSync(''' -// @dart = 2.8 -void main() { -} - -@pragma('vm:entry-point') -void dream() => run(interactive: false); - -@pragma('vm:entry-point', foobar) -void dreamWithFlags() => run(interactive: false); -'''); - final PackageConfig packageConfig = await loadPackageConfigWithLogging( - flutterProject.directory.childDirectory('.dart_tool').childFile('package_config.json'), - logger: globals.logger, - throwOnError: false, - ); - await generateMainDartWithPluginRegistrant( - flutterProject, - packageConfig, - 'package:app/main.dart', - mainFile, - throwOnPluginPubspecError: true, - ); - expect(flutterProject.dartPluginRegistrant.readAsStringSync(), - '//\n' - '// Generated file. Do not edit.\n' - '// This file is generated from template in file `flutter_tools/lib/src/flutter_plugins.dart`.\n' - '//\n' - '\n' - '// @dart = 2.8\n' - '\n' - '// When `package:app/main.dart` defines `main`, that definition is shadowed by the definition below.\n' - "export 'package:app/main.dart';\n" - '\n' - "import 'package:app/main.dart' as entrypoint;\n" "import 'dart:io'; // flutter_ignore: dart_io_import.\n" "import 'package:url_launcher_android/url_launcher_android.dart';\n" "import 'package:url_launcher_ios/url_launcher_ios.dart';\n" @@ -981,20 +781,7 @@ void dreamWithFlags() => run(interactive: false); '\n' ' }\n' ' }\n' - '\n' - '}\n' - '\n' - 'typedef _UnaryFunction = dynamic Function(List args);\n' - 'typedef _NullaryFunction = dynamic Function();\n' - '\n' - 'void main(List args) {\n' - ' if (entrypoint.main is _UnaryFunction) {\n' - ' (entrypoint.main as _UnaryFunction)(args);\n' - ' } else {\n' - ' (entrypoint.main as _NullaryFunction)();\n' - ' }\n' '}\n' - , ); }, overrides: { FileSystem: () => fs, diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart index 8c79acb033aaa..1b0b03e06ad1a 100644 --- a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart @@ -133,85 +133,50 @@ void main() { expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); - testWithoutContext('xcodeVersionSatisfactory is false when version is less than minimum', () { + testWithoutContext('version checks fail when version is less than minimum', () { xcodeProjectInterpreter.isInstalled = true; - xcodeProjectInterpreter.version = Version(9, 0, 0); + xcodeProjectInterpreter.version = Version(9, null, null); expect(xcode.isRequiredVersionSatisfactory, isFalse); - }); - - testWithoutContext('xcodeVersionSatisfactory is false when xcodebuild tools are not installed', () { - xcodeProjectInterpreter.isInstalled = false; - - expect(xcode.isRequiredVersionSatisfactory, isFalse); - }); - - testWithoutContext('xcodeVersionSatisfactory is true when version meets minimum', () { - xcodeProjectInterpreter.isInstalled = true; - xcodeProjectInterpreter.version = Version(12, 3, null); - - expect(xcode.isRequiredVersionSatisfactory, isTrue); - }); - - testWithoutContext('xcodeVersionSatisfactory is true when major version exceeds minimum', () { - xcodeProjectInterpreter.isInstalled = true; - xcodeProjectInterpreter.version = Version(13, 0, 0); - - expect(xcode.isRequiredVersionSatisfactory, isTrue); - }); - - testWithoutContext('xcodeVersionSatisfactory is true when minor version exceeds minimum', () { - xcodeProjectInterpreter.isInstalled = true; - xcodeProjectInterpreter.version = Version(12, 5, 0); - - expect(xcode.isRequiredVersionSatisfactory, isTrue); - }); - - testWithoutContext('xcodeVersionSatisfactory is true when patch version exceeds minimum', () { - xcodeProjectInterpreter.isInstalled = true; - xcodeProjectInterpreter.version = Version(12, 3, 1); - - expect(xcode.isRequiredVersionSatisfactory, isTrue); - }); - - testWithoutContext('isRecommendedVersionSatisfactory is false when version is less than minimum', () { - xcodeProjectInterpreter.isInstalled = true; - xcodeProjectInterpreter.version = Version(12, null, null); - expect(xcode.isRecommendedVersionSatisfactory, isFalse); }); - testWithoutContext('isRecommendedVersionSatisfactory is false when xcodebuild tools are not installed', () { + testWithoutContext('version checks fail when xcodebuild tools are not installed', () { xcodeProjectInterpreter.isInstalled = false; + expect(xcode.isRequiredVersionSatisfactory, isFalse); expect(xcode.isRecommendedVersionSatisfactory, isFalse); }); - testWithoutContext('isRecommendedVersionSatisfactory is true when version meets minimum', () { + testWithoutContext('version checks pass when version meets minimum', () { xcodeProjectInterpreter.isInstalled = true; - xcodeProjectInterpreter.version = Version(13, 0, 0); + xcodeProjectInterpreter.version = Version(13, null, null); + expect(xcode.isRequiredVersionSatisfactory, isTrue); expect(xcode.isRecommendedVersionSatisfactory, isTrue); }); - testWithoutContext('isRecommendedVersionSatisfactory is true when major version exceeds minimum', () { + testWithoutContext('version checks pass when major version exceeds minimum', () { xcodeProjectInterpreter.isInstalled = true; xcodeProjectInterpreter.version = Version(14, 0, 0); + expect(xcode.isRequiredVersionSatisfactory, isTrue); expect(xcode.isRecommendedVersionSatisfactory, isTrue); }); - testWithoutContext('isRecommendedVersionSatisfactory is true when minor version exceeds minimum', () { + testWithoutContext('version checks pass when minor version exceeds minimum', () { xcodeProjectInterpreter.isInstalled = true; xcodeProjectInterpreter.version = Version(13, 3, 0); + expect(xcode.isRequiredVersionSatisfactory, isTrue); expect(xcode.isRecommendedVersionSatisfactory, isTrue); }); - testWithoutContext('isRecommendedVersionSatisfactory is true when patch version exceeds minimum', () { + testWithoutContext('version checks pass when patch version exceeds minimum', () { xcodeProjectInterpreter.isInstalled = true; xcodeProjectInterpreter.version = Version(13, 0, 2); + expect(xcode.isRequiredVersionSatisfactory, isTrue); expect(xcode.isRecommendedVersionSatisfactory, isTrue); }); @@ -232,7 +197,7 @@ void main() { testWithoutContext('isInstalledAndMeetsVersionCheck is true when macOS and installed and version is satisfied', () { xcodeProjectInterpreter.isInstalled = true; - xcodeProjectInterpreter.version = Version(12, 3, null); + xcodeProjectInterpreter.version = Version(13, null, null); expect(xcode.isInstalledAndMeetsVersionCheck, isTrue); expect(fakeProcessManager.hasRemainingExpectations, isFalse); diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_validator_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_validator_test.dart index 4a8451e2e5a4d..26f4223141483 100644 --- a/packages/flutter_tools/test/general.shard/macos/xcode_validator_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/xcode_validator_test.dart @@ -56,7 +56,7 @@ void main() { final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.partial); expect(result.messages.last.type, ValidationMessageType.error); - expect(result.messages.last.message, contains('Flutter requires a minimum Xcode version of 12.3')); + expect(result.messages.last.message, contains('Flutter requires Xcode 13 or higher')); }); testWithoutContext('Emits partial status when Xcode below recommended version', () async { @@ -70,7 +70,7 @@ void main() { expect(result.type, ValidationType.partial); expect(result.messages.last.type, ValidationMessageType.hint); expect(result.messages.last.message, contains('Flutter recommends a minimum Xcode version of 13')); - }); + }, skip: true); // [intended] Unskip and update when minimum and required check versions diverge. testWithoutContext('Emits partial status when Xcode EULA not signed', () async { final ProcessManager processManager = FakeProcessManager.list([ diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart index 67324be3a163a..eed1bc664a9e8 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -1189,8 +1189,8 @@ void main() { ] } '''); - // Start from an empty generated_main.dart file. - globals.fs.directory('.dart_tool').childDirectory('flutter_build').childFile('generated_main.dart').createSync(recursive: true); + // Start from an empty dart_plugin_registrant.dart file. + globals.fs.directory('.dart_tool').childDirectory('flutter_build').childFile('dart_plugin_registrant.dart').createSync(recursive: true); await residentRunner.runSourceGenerators(); @@ -1267,9 +1267,9 @@ flutter: final File generatedMain = globals.fs.directory('.dart_tool') .childDirectory('flutter_build') - .childFile('generated_main.dart'); + .childFile('dart_plugin_registrant.dart'); - expect(generatedMain.readAsStringSync(), contains('custom_main.dart')); + expect(generatedMain.existsSync(), isTrue); expect(testLogger.errorText, isEmpty); expect(testLogger.statusText, isEmpty); })); diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index d1bcdf5c5e885..1e553d661f34d 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -936,7 +936,7 @@ void main() { // While this file should be ignored on web, generating it here will cause a // perf regression in hot restart. - testUsingContext('Does not generate generated_main.dart', () async { + testUsingContext('Does not generate dart_plugin_registrant.dart', () async { // Create necessary files for [DartPluginRegistrantTarget] final File packageConfig = globals.fs.directory('.dart_tool') .childFile('package_config.json'); @@ -954,10 +954,10 @@ void main() { ] } '''); - // Start with a generated_main.dart file. + // Start with a dart_plugin_registrant.dart file. globals.fs.directory('.dart_tool') .childDirectory('flutter_build') - .childFile('generated_main.dart') + .childFile('dart_plugin_registrant.dart') .createSync(recursive: true); final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); @@ -965,7 +965,7 @@ void main() { final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice); await residentWebRunner.runSourceGenerators(); - // generated_main.dart should be untouched, indicating that its + // dart_plugin_registrant.dart should be untouched, indicating that its // generation didn't run. If it had run, the file would have been removed as // there are no plugins in the project. expect(project.dartPluginRegistrant.existsSync(), true); diff --git a/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart b/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart index f2eced8994684..415e87795f4af 100644 --- a/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart +++ b/packages/flutter_tools/test/general.shard/test/test_compiler_test.dart @@ -115,7 +115,7 @@ void main() { Logger: () => BufferLogger.test(), }); - testUsingContext('TestCompiler updates generated_main.dart', () async { + testUsingContext('TestCompiler updates dart_plugin_registrant.dart', () async { final Directory fakeDartPlugin = fileSystem.directory('a_plugin'); fileSystem.file('pubspec.yaml').writeAsStringSync(''' name: foo @@ -152,12 +152,12 @@ environment: final File generatedMain = fileSystem .directory('.dart_tool') .childDirectory('flutter_build') - .childFile('generated_main.dart'); + .childFile('dart_plugin_registrant.dart'); expect(generatedMain, exists); expect( - generatedMain.readAsLinesSync(), - contains("import 'test/foo.dart' as entrypoint;") + generatedMain.readAsStringSync(), + contains('APlugin.registerWith();') ); }, overrides: { FileSystem: () => fileSystem, diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart index 91434767f9cd5..e451e11aa8c12 100644 --- a/packages/flutter_tools/test/src/context.dart +++ b/packages/flutter_tools/test/src/context.dart @@ -294,10 +294,10 @@ class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter { bool get isInstalled => true; @override - String get versionText => 'Xcode 12.3'; + String get versionText => 'Xcode 13'; @override - Version get version => Version(12, 3, null); + Version get version => Version(13, null, null); @override Future> getBuildSettings(