From 99e343a214bad306c18d87a61beea87c9ba36291 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Thu, 15 Jun 2017 12:53:13 -0700 Subject: [PATCH 01/51] Re-title, format iOS device tests (#10733) --- .../flutter_tools/test/ios/devices_test.dart | 129 +++++++++--------- 1 file changed, 61 insertions(+), 68 deletions(-) diff --git a/packages/flutter_tools/test/ios/devices_test.dart b/packages/flutter_tools/test/ios/devices_test.dart index 96ef415e42d38..ec1b995bca500 100644 --- a/packages/flutter_tools/test/ios/devices_test.dart +++ b/packages/flutter_tools/test/ios/devices_test.dart @@ -22,7 +22,7 @@ void main() { final FakePlatform osx = new FakePlatform.fromPlatform(const LocalPlatform()); osx.operatingSystem = 'macos'; - group('test screenshot', () { + group('screenshot', () { MockProcessManager mockProcessManager; MockFile mockOutputFile; IOSDevice iosDeviceUnderTest; @@ -32,75 +32,68 @@ void main() { mockOutputFile = new MockFile(); }); - testUsingContext( - 'screenshot without ideviceinstaller error', - () async { - when(mockOutputFile.path).thenReturn(fs.path.join('some', 'test', 'path', 'image.png')); - // Let everything else return exit code 0 so process.dart doesn't crash. - // The matcher order is important. - when( - mockProcessManager.run(any, environment: null, workingDirectory: null) - ).thenReturn( - new Future.value(new ProcessResult(2, 0, '', '')) - ); - // Let `which idevicescreenshot` fail with exit code 1. - when( - mockProcessManager.runSync( - ['which', 'idevicescreenshot'], environment: null, workingDirectory: null) - ).thenReturn( - new ProcessResult(1, 1, '', '') - ); + testUsingContext('error if idevicescreenshot is not installed', () async { + when(mockOutputFile.path).thenReturn(fs.path.join('some', 'test', 'path', 'image.png')); + // Let everything else return exit code 0 so process.dart doesn't crash. + // The matcher order is important. + when( + mockProcessManager.run(any, environment: null, workingDirectory: null) + ).thenReturn( + new Future.value(new ProcessResult(2, 0, '', '')) + ); + // Let `which idevicescreenshot` fail with exit code 1. + when( + mockProcessManager.runSync( + ['which', 'idevicescreenshot'], environment: null, workingDirectory: null) + ).thenReturn( + new ProcessResult(1, 1, '', '') + ); - iosDeviceUnderTest = new IOSDevice('1234'); - await iosDeviceUnderTest.takeScreenshot(mockOutputFile); - verify(mockProcessManager.runSync( - ['which', 'idevicescreenshot'], environment: null, workingDirectory: null)); - verifyNever(mockProcessManager.run( - ['idevicescreenshot', fs.path.join('some', 'test', 'path', 'image.png')], - environment: null, - workingDirectory: null - )); - expect(testLogger.errorText, contains('brew install ideviceinstaller')); - }, - overrides: { - ProcessManager: () => mockProcessManager, - Platform: () => osx, - } - ); + iosDeviceUnderTest = new IOSDevice('1234'); + await iosDeviceUnderTest.takeScreenshot(mockOutputFile); + verify(mockProcessManager.runSync( + ['which', 'idevicescreenshot'], environment: null, workingDirectory: null)); + verifyNever(mockProcessManager.run( + ['idevicescreenshot', fs.path.join('some', 'test', 'path', 'image.png')], + environment: null, + workingDirectory: null + )); + expect(testLogger.errorText, contains('brew install ideviceinstaller')); + }, + overrides: { + ProcessManager: () => mockProcessManager, + Platform: () => osx, + }); - testUsingContext( - 'screenshot with ideviceinstaller gets command', - () async { - when(mockOutputFile.path).thenReturn(fs.path.join('some', 'test', 'path', 'image.png')); - // Let everything else return exit code 0. - // The matcher order is important. - when( - mockProcessManager.run(any, environment: null, workingDirectory: null) - ).thenReturn( - new Future.value(new ProcessResult(4, 0, '', '')) - ); - // Let there be idevicescreenshot in the PATH. - when( - mockProcessManager.runSync( - ['which', 'idevicescreenshot'], environment: null, workingDirectory: null) - ).thenReturn( - new ProcessResult(3, 0, fs.path.join('some', 'path', 'to', 'iscreenshot'), '') - ); + testUsingContext('idevicescreenshot captures and returns screenshot', () async { + when(mockOutputFile.path).thenReturn(fs.path.join('some', 'test', 'path', 'image.png')); + // Let everything else return exit code 0. + // The matcher order is important. + when( + mockProcessManager.run(any, environment: null, workingDirectory: null) + ).thenReturn( + new Future.value(new ProcessResult(4, 0, '', '')) + ); + // Let there be idevicescreenshot in the PATH. + when( + mockProcessManager.runSync( + ['which', 'idevicescreenshot'], environment: null, workingDirectory: null) + ).thenReturn( + new ProcessResult(3, 0, fs.path.join('some', 'path', 'to', 'iscreenshot'), '') + ); - iosDeviceUnderTest = new IOSDevice('1234'); - await iosDeviceUnderTest.takeScreenshot(mockOutputFile); - verify(mockProcessManager.runSync( - ['which', 'idevicescreenshot'], environment: null, workingDirectory: null)); - verify(mockProcessManager.run( - [ - fs.path.join('some', 'path', 'to', 'iscreenshot'), - fs.path.join('some', 'test', 'path', 'image.png') - ], - environment: null, - workingDirectory: null - )); - }, - overrides: {ProcessManager: () => mockProcessManager} - ); + iosDeviceUnderTest = new IOSDevice('1234'); + await iosDeviceUnderTest.takeScreenshot(mockOutputFile); + verify(mockProcessManager.runSync( + ['which', 'idevicescreenshot'], environment: null, workingDirectory: null)); + verify(mockProcessManager.run( + [ + fs.path.join('some', 'path', 'to', 'iscreenshot'), + fs.path.join('some', 'test', 'path', 'image.png') + ], + environment: null, + workingDirectory: null + )); + }, overrides: {ProcessManager: () => mockProcessManager}); }); } From d1feb1ad5f0a8bf036b48093b1f949d754abf5b4 Mon Sep 17 00:00:00 2001 From: Brian Slesinsky Date: Thu, 15 Jun 2017 14:39:46 -0700 Subject: [PATCH 02/51] Coverage: fix installHook API (#10747) - remove unused collector parameter - clarify a comment - inline _currentPackageTestDir --- packages/flutter_tools/lib/src/commands/test.dart | 10 +++------- .../flutter_tools/lib/src/test/flutter_platform.dart | 2 -- packages/flutter_tools/lib/src/test/runner.dart | 4 ++-- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index 6171abcc88b97..a26ea295e2dd2 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -72,12 +72,6 @@ class TestCommand extends FlutterCommand { @override String get description => 'Run Flutter unit tests for the current project.'; - Directory get _currentPackageTestDir { - // We don't scan the entire package, only the test/ subdirectory, so that - // files with names like like "hit_test.dart" don't get run. - return fs.directory('test'); - } - Future _collectCoverageData(CoverageCollector collector, { bool mergeCoverageData: false }) async { final Status status = logger.startProgress('Collecting coverage information...'); final String coverageData = await collector.finalizeCoverage( @@ -159,7 +153,9 @@ class TestCommand extends FlutterCommand { Directory workDir; if (files.isEmpty) { - workDir = _currentPackageTestDir; + // We don't scan the entire package, only the test/ subdirectory, so that + // files with names like like "hit_test.dart" don't get run. + workDir = fs.directory('test'); if (!workDir.existsSync()) throwToolExit('Test directory "${workDir.path}" not found.'); files = _findTests(workDir); diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart index 8872f79004942..e3999f355d483 100644 --- a/packages/flutter_tools/lib/src/test/flutter_platform.dart +++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart @@ -18,7 +18,6 @@ import '../base/io.dart'; import '../base/process_manager.dart'; import '../dart/package_map.dart'; import '../globals.dart'; -import 'coverage_collector.dart'; import 'watcher.dart'; /// The timeout we give the test process to connect to the test harness @@ -53,7 +52,6 @@ final Map _kHosts = runTests( serverType: serverType, ); - // Set the package path used for child processes. - // TODO(skybrian): why is this global? Move to installHook? + // Make the global packages path absolute. + // (Makes sure it still works after we change the current directory.) PackageMap.globalPackagesPath = fs.path.normalize(fs.path.absolute(PackageMap.globalPackagesPath)); From 615410d2d2336e83e7b8496e2e6e8029dc438257 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Thu, 15 Jun 2017 16:11:08 -0700 Subject: [PATCH 03/51] Inject iOS, Android workflows via context (#10750) Eliminates the need for the device/daemon code to get at the iOS/Android tooling indirectly via Doctor. In tests, we now inject the workflow objects (or mocks) directly. --- .../lib/src/android/android_device.dart | 4 +-- .../lib/src/android/android_workflow.dart | 3 ++ packages/flutter_tools/lib/src/doctor.dart | 20 +++---------- .../flutter_tools/lib/src/ios/devices.dart | 6 ++-- .../lib/src/ios/ios_workflow.dart | 3 ++ packages/flutter_tools/lib/src/ios/mac.dart | 8 ++--- .../flutter_tools/lib/src/ios/simulators.dart | 4 +-- .../test/commands/daemon_test.dart | 30 ++++++------------- 8 files changed, 30 insertions(+), 48 deletions(-) diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index b4dd532fbd59f..f6ed675c7ac93 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:convert'; import '../android/android_sdk.dart'; +import '../android/android_workflow.dart'; import '../application_package.dart'; import '../base/common.dart' show throwToolExit; import '../base/file_system.dart'; @@ -17,7 +18,6 @@ import '../base/process_manager.dart'; import '../build_info.dart'; import '../commands/build_apk.dart'; import '../device.dart'; -import '../doctor.dart'; import '../globals.dart'; import '../protocol_discovery.dart'; @@ -45,7 +45,7 @@ class AndroidDevices extends PollingDeviceDiscovery { bool get supportsPlatform => true; @override - bool get canListAnything => doctor.androidWorkflow.canListDevices; + bool get canListAnything => androidWorkflow.canListDevices; @override List pollingGetDevices() => getAdbDevices(); diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart index aedfca7cd06ca..c01d5af812295 100644 --- a/packages/flutter_tools/lib/src/android/android_workflow.dart +++ b/packages/flutter_tools/lib/src/android/android_workflow.dart @@ -4,6 +4,7 @@ import 'dart:async'; +import '../base/context.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/os.dart'; @@ -15,6 +16,8 @@ import '../globals.dart'; import 'android_sdk.dart'; import 'android_studio.dart' as android_studio; +AndroidWorkflow get androidWorkflow => context.putIfAbsent(AndroidWorkflow, () => new AndroidWorkflow()); + class AndroidWorkflow extends DoctorValidator implements Workflow { AndroidWorkflow() : super('Android toolchain - develop for Android devices'); diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index d01e42f8b4288..39eba5727ebee 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -27,18 +27,6 @@ import 'version.dart'; Doctor get doctor => context[Doctor]; class Doctor { - Doctor() { - _androidWorkflow = new AndroidWorkflow(); - _iosWorkflow = new IOSWorkflow(); - } - - IOSWorkflow _iosWorkflow; - AndroidWorkflow _androidWorkflow; - - IOSWorkflow get iosWorkflow => _iosWorkflow; - - AndroidWorkflow get androidWorkflow => _androidWorkflow; - List _validators; List get validators { @@ -46,11 +34,11 @@ class Doctor { _validators = []; _validators.add(new _FlutterValidator()); - if (_androidWorkflow.appliesToHostPlatform) - _validators.add(_androidWorkflow); + if (androidWorkflow.appliesToHostPlatform) + _validators.add(androidWorkflow); - if (_iosWorkflow.appliesToHostPlatform) - _validators.add(_iosWorkflow); + if (iosWorkflow.appliesToHostPlatform) + _validators.add(iosWorkflow); final List ideValidators = []; ideValidators.addAll(AndroidStudioValidator.allValidators); diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 396266fb231d4..dc170ec94f1f0 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -14,9 +14,9 @@ import '../base/process.dart'; import '../base/process_manager.dart'; import '../build_info.dart'; import '../device.dart'; -import '../doctor.dart'; import '../globals.dart'; import '../protocol_discovery.dart'; +import 'ios_workflow.dart'; import 'mac.dart'; const String _kIdeviceinstallerInstructions = @@ -33,7 +33,7 @@ class IOSDevices extends PollingDeviceDiscovery { bool get supportsPlatform => platform.isMacOS; @override - bool get canListAnything => doctor.iosWorkflow.canListDevices; + bool get canListAnything => iosWorkflow.canListDevices; @override List pollingGetDevices() => IOSDevice.getAttachedDevices(); @@ -97,7 +97,7 @@ class IOSDevice extends Device { bool get supportsStartPaused => false; static List getAttachedDevices() { - if (!doctor.iosWorkflow.hasIDeviceId) + if (!iosWorkflow.hasIDeviceId) return []; final List devices = []; diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart index 696048b221c86..4c640aade2d16 100644 --- a/packages/flutter_tools/lib/src/ios/ios_workflow.dart +++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart @@ -5,6 +5,7 @@ import 'dart:async'; import '../base/common.dart'; +import '../base/context.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/os.dart'; @@ -14,6 +15,8 @@ import '../base/version.dart'; import '../doctor.dart'; import 'mac.dart'; +IOSWorkflow get iosWorkflow => context.putIfAbsent(IOSWorkflow, () => new IOSWorkflow()); + Xcode get xcode => Xcode.instance; class IOSWorkflow extends DoctorValidator implements Workflow { diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 16430aa207fab..ef9188af84c01 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -17,12 +17,12 @@ import '../base/platform.dart'; import '../base/process.dart'; import '../base/process_manager.dart'; import '../build_info.dart'; -import '../doctor.dart'; import '../flx.dart' as flx; import '../globals.dart'; import '../plugins.dart'; import '../services.dart'; import 'code_signing.dart'; +import 'ios_workflow.dart'; import 'xcodeproj.dart'; const int kXcodeRequiredVersionMajor = 7; @@ -354,8 +354,8 @@ final String cocoaPodsUpgradeInstructions = ''' Future _runPodInstall(Directory bundle, String engineDirectory) async { if (fs.file(fs.path.join(bundle.path, 'Podfile')).existsSync()) { - if (!await doctor.iosWorkflow.isCocoaPodsInstalledAndMeetsVersionCheck) { - final String minimumVersion = doctor.iosWorkflow.cocoaPodsMinimumVersion; + if (!await iosWorkflow.isCocoaPodsInstalledAndMeetsVersionCheck) { + final String minimumVersion = iosWorkflow.cocoaPodsMinimumVersion; printError( 'Warning: CocoaPods version $minimumVersion or greater not installed. Skipping pod install.\n' '$noCocoaPodsConsequence\n' @@ -365,7 +365,7 @@ Future _runPodInstall(Directory bundle, String engineDirectory) async { ); return; } - if (!await doctor.iosWorkflow.isCocoaPodsInitialized) { + if (!await iosWorkflow.isCocoaPodsInitialized) { printError( 'Warning: CocoaPods installed but not initialized. Skipping pod install.\n' '$noCocoaPodsConsequence\n' diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index d4ec64f4f5084..d67213bb11d88 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -16,10 +16,10 @@ import '../base/process.dart'; import '../base/process_manager.dart'; import '../build_info.dart'; import '../device.dart'; -import '../doctor.dart'; import '../flx.dart' as flx; import '../globals.dart'; import '../protocol_discovery.dart'; +import 'ios_workflow.dart'; import 'mac.dart'; const String _xcrunPath = '/usr/bin/xcrun'; @@ -34,7 +34,7 @@ class IOSSimulators extends PollingDeviceDiscovery { bool get supportsPlatform => platform.isMacOS; @override - bool get canListAnything => doctor.iosWorkflow.canListDevices; + bool get canListAnything => iosWorkflow.canListDevices; @override List pollingGetDevices() => IOSSimulatorUtils.instance.getAttachedDevices(); diff --git a/packages/flutter_tools/test/commands/daemon_test.dart b/packages/flutter_tools/test/commands/daemon_test.dart index e55d7ddb28368..10a864be92724 100644 --- a/packages/flutter_tools/test/commands/daemon_test.dart +++ b/packages/flutter_tools/test/commands/daemon_test.dart @@ -8,7 +8,6 @@ import 'package:flutter_tools/src/android/android_workflow.dart'; import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/commands/daemon.dart'; -import 'package:flutter_tools/src/doctor.dart'; import 'package:flutter_tools/src/globals.dart'; import 'package:flutter_tools/src/ios/ios_workflow.dart'; import 'package:test/test.dart'; @@ -220,7 +219,8 @@ void main() { expect(response['params'], containsPair('title', 'Unable to list devices')); expect(response['params'], containsPair('message', contains('Unable to discover Android devices'))); }, overrides: { - Doctor: () => new MockDoctor(androidCanListDevices: false), + AndroidWorkflow: () => new MockAndroidWorkflow(canListDevices: false), + IOSWorkflow: () => new MockIOSWorkflow(), }); testUsingContext('device.getDevices should respond with list', () async { @@ -263,36 +263,24 @@ void main() { commands.close(); }); }, overrides: { - Doctor: () => new MockDoctor(), + AndroidWorkflow: () => new MockAndroidWorkflow(), + IOSWorkflow: () => new MockIOSWorkflow(), }); }); } bool _notEvent(Map map) => map['event'] == null; -class MockDoctor extends Doctor { - final bool androidCanListDevices; - final bool iosCanListDevices; - - MockDoctor({this.androidCanListDevices: true, this.iosCanListDevices: true}); - - @override - AndroidWorkflow get androidWorkflow => new MockAndroidWorkflow(androidCanListDevices); - - @override - IOSWorkflow get iosWorkflow => new MockIosWorkflow(iosCanListDevices); -} - class MockAndroidWorkflow extends AndroidWorkflow { + MockAndroidWorkflow({ this.canListDevices: true }); + @override final bool canListDevices; - - MockAndroidWorkflow(this.canListDevices); } -class MockIosWorkflow extends IOSWorkflow { +class MockIOSWorkflow extends IOSWorkflow { + MockIOSWorkflow({ this.canListDevices:true }); + @override final bool canListDevices; - - MockIosWorkflow(this.canListDevices); } From bd67926f84e8d6b94ad111a508e066b682a125fa Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Thu, 15 Jun 2017 17:54:45 -0700 Subject: [PATCH 04/51] Run coverage in a dedicated shard on Travis (#10755) --- .travis.yml | 1 + dev/bots/test.dart | 188 ++++++++++++++++++++++---------------- dev/bots/travis_upload.sh | 2 +- 3 files changed, 109 insertions(+), 82 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6f8e95c09d183..7528cb4d07a23 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ install: env: - SHARD=analyze - SHARD=tests + - SHARD=coverage - SHARD=docs before_script: - ./dev/bots/travis_setup.sh diff --git a/dev/bots/test.dart b/dev/bots/test.dart index dd62802334aff..a978a543d17b1 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -8,6 +8,8 @@ import 'dart:io'; import 'package:path/path.dart' as path; +typedef Future ShardRunner(); + final String flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script)))); final String flutter = path.join(flutterRoot, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter'); final String dart = path.join(flutterRoot, 'bin', 'cache', 'dart-sdk', 'bin', Platform.isWindows ? 'dart.exe' : 'dart'); @@ -22,6 +24,13 @@ final String yellow = hasColor ? '\x1B[33m' : ''; final String cyan = hasColor ? '\x1B[36m' : ''; final String reset = hasColor ? '\x1B[0m' : ''; +const Map _kShards = const { + 'docs': _generateDocs, + 'analyze': _analyzeRepo, + 'tests': _runTests, + 'coverage': _runCoverage, +}; + /// When you call this, you can set FLUTTER_TEST_ARGS to pass custom /// arguments to flutter test. For example, you might want to call this /// script using FLUTTER_TEST_ARGS=--local-engine=host_debug_unopt to @@ -33,94 +42,111 @@ final String reset = hasColor ? '\x1B[0m' : ''; /// SHARD=analyze bin/cache/dart-sdk/bin/dart dev/bots/test.dart /// FLUTTER_TEST_ARGS=--local-engine=host_debug_unopt bin/cache/dart-sdk/bin/dart dev/bots/test.dart Future main() async { - if (Platform.environment['SHARD'] == 'docs') { - print('${bold}DONE: test.dart does nothing in the docs shard.$reset'); - } else if (Platform.environment['SHARD'] == 'analyze') { - // Analyze all the Dart code in the repo. - await _runFlutterAnalyze(flutterRoot, - options: ['--flutter-repo'], - ); + final String shard = Platform.environment['SHARD'] ?? 'tests'; + if (!_kShards.containsKey(shard)) + throw new ArgumentError('Invalid shard: $shard'); + await _kShards[shard](); +} - // Analyze all the sample code in the repo - await _runCommand(dart, [path.join(flutterRoot, 'dev', 'bots', 'analyze-sample-code.dart')], - workingDirectory: flutterRoot, - ); +Future _generateDocs() async { + print('${bold}DONE: test.dart does nothing in the docs shard.$reset'); +} - // Try with the --watch analyzer, to make sure it returns success also. - // The --benchmark argument exits after one run. - await _runFlutterAnalyze(flutterRoot, - options: ['--flutter-repo', '--watch', '--benchmark'], - ); +Future _analyzeRepo() async { + // Analyze all the Dart code in the repo. + await _runFlutterAnalyze(flutterRoot, + options: ['--flutter-repo'], + ); - // Try an analysis against a big version of the gallery. - await _runCommand(dart, [path.join(flutterRoot, 'dev', 'tools', 'mega_gallery.dart')], - workingDirectory: flutterRoot, - ); - await _runFlutterAnalyze(path.join(flutterRoot, 'dev', 'benchmarks', 'mega_gallery'), - options: ['--watch', '--benchmark'], - ); + // Analyze all the sample code in the repo + await _runCommand(dart, [path.join(flutterRoot, 'dev', 'bots', 'analyze-sample-code.dart')], + workingDirectory: flutterRoot, + ); - print('${bold}DONE: Analysis successful.$reset'); - } else { - // Verify that the tests actually return failure on failure and success on success. - final String automatedTests = path.join(flutterRoot, 'dev', 'automated_tests'); - await _runFlutterTest(automatedTests, - script: path.join('test_smoke_test', 'fail_test.dart'), - expectFailure: true, - printOutput: false, - ); - await _runFlutterTest(automatedTests, - script: path.join('test_smoke_test', 'pass_test.dart'), - printOutput: false, - ); - await _runFlutterTest(automatedTests, - script: path.join('test_smoke_test', 'crash1_test.dart'), - expectFailure: true, - printOutput: false, - ); - await _runFlutterTest(automatedTests, - script: path.join('test_smoke_test', 'crash2_test.dart'), - expectFailure: true, - printOutput: false, - ); - await _runFlutterTest(automatedTests, - script: path.join('test_smoke_test', 'syntax_error_test.broken_dart'), - expectFailure: true, - printOutput: false, - ); - await _runFlutterTest(automatedTests, - script: path.join('test_smoke_test', 'missing_import_test.broken_dart'), - expectFailure: true, - printOutput: false, - ); - await _runCommand(flutter, ['drive', '--use-existing-app', '-t', path.join('test_driver', 'failure.dart')], - workingDirectory: path.join(flutterRoot, 'packages', 'flutter_driver'), - expectFailure: true, - printOutput: false, - ); + // Try with the --watch analyzer, to make sure it returns success also. + // The --benchmark argument exits after one run. + await _runFlutterAnalyze(flutterRoot, + options: ['--flutter-repo', '--watch', '--benchmark'], + ); - final List coverageFlags = []; - if (Platform.environment['TRAVIS'] != null && Platform.environment['TRAVIS_PULL_REQUEST'] == 'false') - coverageFlags.add('--coverage'); + // Try an analysis against a big version of the gallery. + await _runCommand(dart, [path.join(flutterRoot, 'dev', 'tools', 'mega_gallery.dart')], + workingDirectory: flutterRoot, + ); + await _runFlutterAnalyze(path.join(flutterRoot, 'dev', 'benchmarks', 'mega_gallery'), + options: ['--watch', '--benchmark'], + ); - // Run tests. - await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter'), - options: coverageFlags, - ); - await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_driver')); - await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_test')); - await _pubRunTest(path.join(flutterRoot, 'packages', 'flutter_tools')); - - await _runAllDartTests(path.join(flutterRoot, 'dev', 'devicelab')); - await _runFlutterTest(path.join(flutterRoot, 'dev', 'manual_tests')); - await _runFlutterTest(path.join(flutterRoot, 'examples', 'hello_world')); - await _runFlutterTest(path.join(flutterRoot, 'examples', 'layers')); - await _runFlutterTest(path.join(flutterRoot, 'examples', 'stocks')); - await _runFlutterTest(path.join(flutterRoot, 'examples', 'flutter_gallery')); - await _runFlutterTest(path.join(flutterRoot, 'examples', 'catalog')); - - print('${bold}DONE: All tests successful.$reset'); + print('${bold}DONE: Analysis successful.$reset'); +} + +Future _runTests() async { + // Verify that the tests actually return failure on failure and success on success. + final String automatedTests = path.join(flutterRoot, 'dev', 'automated_tests'); + await _runFlutterTest(automatedTests, + script: path.join('test_smoke_test', 'fail_test.dart'), + expectFailure: true, + printOutput: false, + ); + await _runFlutterTest(automatedTests, + script: path.join('test_smoke_test', 'pass_test.dart'), + printOutput: false, + ); + await _runFlutterTest(automatedTests, + script: path.join('test_smoke_test', 'crash1_test.dart'), + expectFailure: true, + printOutput: false, + ); + await _runFlutterTest(automatedTests, + script: path.join('test_smoke_test', 'crash2_test.dart'), + expectFailure: true, + printOutput: false, + ); + await _runFlutterTest(automatedTests, + script: path.join('test_smoke_test', 'syntax_error_test.broken_dart'), + expectFailure: true, + printOutput: false, + ); + await _runFlutterTest(automatedTests, + script: path.join('test_smoke_test', 'missing_import_test.broken_dart'), + expectFailure: true, + printOutput: false, + ); + await _runCommand(flutter, ['drive', '--use-existing-app', '-t', path.join('test_driver', 'failure.dart')], + workingDirectory: path.join(flutterRoot, 'packages', 'flutter_driver'), + expectFailure: true, + printOutput: false, + ); + + // Run tests. + await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter')); + await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_driver')); + await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_test')); + await _pubRunTest(path.join(flutterRoot, 'packages', 'flutter_tools')); + + await _runAllDartTests(path.join(flutterRoot, 'dev', 'devicelab')); + await _runFlutterTest(path.join(flutterRoot, 'dev', 'manual_tests')); + await _runFlutterTest(path.join(flutterRoot, 'examples', 'hello_world')); + await _runFlutterTest(path.join(flutterRoot, 'examples', 'layers')); + await _runFlutterTest(path.join(flutterRoot, 'examples', 'stocks')); + await _runFlutterTest(path.join(flutterRoot, 'examples', 'flutter_gallery')); + await _runFlutterTest(path.join(flutterRoot, 'examples', 'catalog')); + + print('${bold}DONE: All tests successful.$reset'); +} + +Future _runCoverage() async { + if (Platform.environment['TRAVIS'] == null || + Platform.environment['TRAVIS_PULL_REQUEST'] != 'false') { + print('${bold}DONE: test.dart does not run coverage for Travis pull requests'); + return; } + + await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter'), + options: const ['--coverage'], + ); + + print('${bold}DONE: Coverage collection successful.$reset'); } Future _pubRunTest( diff --git a/dev/bots/travis_upload.sh b/dev/bots/travis_upload.sh index 7d42d923f1352..7201dfd426b45 100755 --- a/dev/bots/travis_upload.sh +++ b/dev/bots/travis_upload.sh @@ -6,7 +6,7 @@ export PATH="$PWD/bin:$PWD/bin/cache/dart-sdk/bin:$PATH" LCOV_FILE=./packages/flutter/coverage/lcov.info -if [ "$SHARD" = "tests" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ -f "$LCOV_FILE" ]; then +if [ "$SHARD" = "coverage" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ -f "$LCOV_FILE" ]; then GSUTIL=$HOME/google-cloud-sdk/bin/gsutil GCLOUD=$HOME/google-cloud-sdk/bin/gcloud From 4b957bf02870c26ef5248e81a2ce5fd15efd281c Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Thu, 15 Jun 2017 18:23:37 -0700 Subject: [PATCH 05/51] Keep provisioning step (#10736) --- dev/devicelab/lib/framework/ios.dart | 23 ++++++++-------- dev/devicelab/lib/tasks/gallery.dart | 5 +--- .../lib/tasks/integration_tests.dart | 5 +--- dev/devicelab/lib/tasks/integration_ui.dart | 5 +--- dev/devicelab/lib/tasks/perf_tests.dart | 26 +++---------------- .../channels/ios/Flutter/Debug.xcconfig | 2 -- .../channels/ios/Flutter/Release.xcconfig | 2 -- .../channels/ios/Flutter/TestConfig.xcconfig | 5 ---- .../ios/Runner.xcodeproj/project.pbxproj | 14 +++++----- 9 files changed, 25 insertions(+), 62 deletions(-) delete mode 100644 dev/integration_tests/channels/ios/Flutter/TestConfig.xcconfig diff --git a/dev/devicelab/lib/framework/ios.dart b/dev/devicelab/lib/framework/ios.dart index 28f726316c20d..b9119acf20bff 100644 --- a/dev/devicelab/lib/framework/ios.dart +++ b/dev/devicelab/lib/framework/ios.dart @@ -16,11 +16,17 @@ const String _kTestXcconfigFileName = 'TestConfig.xcconfig'; const FileSystem _fs = const io.LocalFileSystem(); /// Patches the given Xcode project adding provisioning certificates and team -/// information required to build and run the project. +/// information required to build and run the project, if +/// FLUTTER_DEVICELAB_XCODE_PROVISIONING_CONFIG is set. If it is not set, +/// we rely on automatic signing by Xcode. Future prepareProvisioningCertificates(String flutterProjectPath) async { final String certificateConfig = await _readProvisioningConfigFile(); - await _patchXcconfigFilesIfNotPatched(flutterProjectPath); + if (certificateConfig == null) { + // No cert config available, rely on automatic signing by Xcode. + return; + } + await _patchXcconfigFilesIfNotPatched(flutterProjectPath); final File testXcconfig = _fs.file(path.join(flutterProjectPath, 'ios/Flutter/$_kTestXcconfigFileName')); await testXcconfig.writeAsString(certificateConfig); } @@ -76,18 +82,11 @@ $specificMessage } if (!dart_io.Platform.environment.containsKey(_kProvisioningConfigFileEnvironmentVariable)) { - throwUsageError(''' + print(''' $_kProvisioningConfigFileEnvironmentVariable variable is not defined in your -environment. Please, define it and try again. - -Example provisioning xcconfig: - -ProvisioningStyle=Manual -CODE_SIGN_IDENTITY=... -PROVISIONING_PROFILE=... -DEVELOPMENT_TEAM=... -PROVISIONING_PROFILE_SPECIFIER=... +environment. Relying on automatic signing by Xcode... '''.trim()); + return null; } final String filePath = dart_io.Platform.environment[_kProvisioningConfigFileEnvironmentVariable]; diff --git a/dev/devicelab/lib/tasks/gallery.dart b/dev/devicelab/lib/tasks/gallery.dart index b0730033bd35e..418ff3ab3bf1e 100644 --- a/dev/devicelab/lib/tasks/gallery.dart +++ b/dev/devicelab/lib/tasks/gallery.dart @@ -31,11 +31,8 @@ class GalleryTransitionTest { await inDirectory(galleryDirectory, () async { await flutter('packages', options: ['get']); - if (deviceOperatingSystem == DeviceOperatingSystem.ios) { + if (deviceOperatingSystem == DeviceOperatingSystem.ios) await prepareProvisioningCertificates(galleryDirectory.path); - // This causes an Xcode project to be created. - await flutter('build', options: ['ios', '--profile']); - } final String testDriver = semanticsEnabled ? 'transitions_perf_with_semantics.dart' diff --git a/dev/devicelab/lib/tasks/integration_tests.dart b/dev/devicelab/lib/tasks/integration_tests.dart index 004e7e593922f..557e245bce5c7 100644 --- a/dev/devicelab/lib/tasks/integration_tests.dart +++ b/dev/devicelab/lib/tasks/integration_tests.dart @@ -37,11 +37,8 @@ class DriverTest { final String deviceId = device.deviceId; await flutter('packages', options: ['get']); - if (deviceOperatingSystem == DeviceOperatingSystem.ios) { + if (deviceOperatingSystem == DeviceOperatingSystem.ios) await prepareProvisioningCertificates(testDirectory); - // This causes an Xcode project to be created. - await flutter('build', options: ['ios', '--profile']); - } await flutter('drive', options: [ '-v', diff --git a/dev/devicelab/lib/tasks/integration_ui.dart b/dev/devicelab/lib/tasks/integration_ui.dart index 0bbcf1969ea57..ddbab5f788fdd 100644 --- a/dev/devicelab/lib/tasks/integration_ui.dart +++ b/dev/devicelab/lib/tasks/integration_ui.dart @@ -18,11 +18,8 @@ Future runEndToEndTests() async { await inDirectory(testDirectory, () async { await flutter('packages', options: ['get']); - if (deviceOperatingSystem == DeviceOperatingSystem.ios) { + if (deviceOperatingSystem == DeviceOperatingSystem.ios) await prepareProvisioningCertificates(testDirectory.path); - // This causes an Xcode project to be created. - await flutter('build', options: ['ios', 'lib/keyboard_resize.dart']); - } await flutter('drive', options: ['-d', deviceId, '-t', 'lib/keyboard_resize.dart']); }); diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart index 282c7cdfc873e..900e02280177e 100644 --- a/dev/devicelab/lib/tasks/perf_tests.dart +++ b/dev/devicelab/lib/tasks/perf_tests.dart @@ -73,9 +73,6 @@ TaskFunction createFlutterViewStartupTest() { return new StartupTest( '${flutterDirectory.path}/examples/flutter_view', reportMetrics: false, - // This project has a non-standard CocoaPods Podfile. Run pod install - // before building the project. - runPodInstall: true, ); } @@ -83,27 +80,18 @@ TaskFunction createFlutterViewStartupTest() { class StartupTest { static const Duration _startupTimeout = const Duration(minutes: 5); - const StartupTest(this.testDirectory, { this.reportMetrics: true, this.runPodInstall: false }); + const StartupTest(this.testDirectory, { this.reportMetrics: true }); final String testDirectory; final bool reportMetrics; - /// Used to trigger a `pod install` when the project has a custom Podfile and - /// flutter build ios won't automatically run `pod install` via the managed - /// plugin system. - final bool runPodInstall; Future call() async { return await inDirectory(testDirectory, () async { final String deviceId = (await devices.workingDevice).deviceId; await flutter('packages', options: ['get']); - if (deviceOperatingSystem == DeviceOperatingSystem.ios) { - if (runPodInstall) - await runPodInstallForCustomPodfile(testDirectory); + if (deviceOperatingSystem == DeviceOperatingSystem.ios) await prepareProvisioningCertificates(testDirectory); - // This causes an Xcode project to be created. - await flutter('build', options: ['ios', '--profile']); - } await flutter('run', options: [ '--verbose', @@ -141,11 +129,8 @@ class PerfTest { final String deviceId = device.deviceId; await flutter('packages', options: ['get']); - if (deviceOperatingSystem == DeviceOperatingSystem.ios) { + if (deviceOperatingSystem == DeviceOperatingSystem.ios) await prepareProvisioningCertificates(testDirectory); - // This causes an Xcode project to be created. - await flutter('build', options: ['ios', '--profile']); - } await flutter('drive', options: [ '-v', @@ -280,11 +265,8 @@ class MemoryTest { final String deviceId = device.deviceId; await flutter('packages', options: ['get']); - if (deviceOperatingSystem == DeviceOperatingSystem.ios) { + if (deviceOperatingSystem == DeviceOperatingSystem.ios) await prepareProvisioningCertificates(testDirectory); - // This causes an Xcode project to be created. - await flutter('build', options: ['ios', '--profile']); - } final int observatoryPort = await findAvailablePort(); diff --git a/dev/integration_tests/channels/ios/Flutter/Debug.xcconfig b/dev/integration_tests/channels/ios/Flutter/Debug.xcconfig index 5c0c17075260c..592ceee85b89b 100644 --- a/dev/integration_tests/channels/ios/Flutter/Debug.xcconfig +++ b/dev/integration_tests/channels/ios/Flutter/Debug.xcconfig @@ -1,3 +1 @@ #include "Generated.xcconfig" - -#include "TestConfig.xcconfig" diff --git a/dev/integration_tests/channels/ios/Flutter/Release.xcconfig b/dev/integration_tests/channels/ios/Flutter/Release.xcconfig index 5c0c17075260c..592ceee85b89b 100644 --- a/dev/integration_tests/channels/ios/Flutter/Release.xcconfig +++ b/dev/integration_tests/channels/ios/Flutter/Release.xcconfig @@ -1,3 +1 @@ #include "Generated.xcconfig" - -#include "TestConfig.xcconfig" diff --git a/dev/integration_tests/channels/ios/Flutter/TestConfig.xcconfig b/dev/integration_tests/channels/ios/Flutter/TestConfig.xcconfig deleted file mode 100644 index 6c52ef8d549af..0000000000000 --- a/dev/integration_tests/channels/ios/Flutter/TestConfig.xcconfig +++ /dev/null @@ -1,5 +0,0 @@ -ProvisioningStyle=Manual -CODE_SIGN_IDENTITY=iPhone Developer -PROVISIONING_PROFILE=Xcode Managed Profile -DEVELOPMENT_TEAM=... -PROVISIONING_PROFILE_SPECIFIER=... diff --git a/dev/integration_tests/channels/ios/Runner.xcodeproj/project.pbxproj b/dev/integration_tests/channels/ios/Runner.xcodeproj/project.pbxproj index 90c96d998a0c4..f5c72c63871f0 100644 --- a/dev/integration_tests/channels/ios/Runner.xcodeproj/project.pbxproj +++ b/dev/integration_tests/channels/ios/Runner.xcodeproj/project.pbxproj @@ -7,10 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 74970F741EDC3266000507F3 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 74970F731EDC3266000507F3 /* GeneratedPluginRegistrant.m */; }; 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; @@ -39,10 +39,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 74970F721EDC3266000507F3 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 74970F731EDC3266000507F3 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -91,6 +91,7 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, ); sourceTree = ""; }; @@ -105,8 +106,6 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( - 74970F721EDC3266000507F3 /* GeneratedPluginRegistrant.h */, - 74970F731EDC3266000507F3 /* GeneratedPluginRegistrant.m */, 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, @@ -114,6 +113,8 @@ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, ); path = Runner; sourceTree = ""; @@ -160,7 +161,6 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = ...; }; }; }; @@ -237,7 +237,7 @@ files = ( 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 97C146F31CF9000F007C117D /* main.m in Sources */, - 74970F741EDC3266000507F3 /* GeneratedPluginRegistrant.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 432ffde52e0065120da2a6e5c12d4eaa6c6345d8 Mon Sep 17 00:00:00 2001 From: xster Date: Thu, 15 Jun 2017 18:25:09 -0700 Subject: [PATCH 06/51] remove usages of booted (#10741) --- packages/flutter_tools/lib/src/ios/simulators.dart | 12 ++++++------ packages/flutter_tools/test/ios/simulators_test.dart | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index d67213bb11d88..02ec3ddda71a5 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -228,12 +228,12 @@ class SimControl { bool _isAnyConnected() => getConnectedDevices().isNotEmpty; - Future isInstalled(String appId) { + Future isInstalled(String deviceId, String appId) { return exitsHappyAsync([ _xcrunPath, 'simctl', 'get_app_container', - 'booted', + deviceId, appId, ]); } @@ -253,8 +253,8 @@ class SimControl { return runCheckedAsync(args); } - Future takeScreenshot(String outputPath) { - return runCheckedAsync([_xcrunPath, 'simctl', 'io', 'booted', 'screenshot', outputPath]); + Future takeScreenshot(String deviceId, String outputPath) { + return runCheckedAsync([_xcrunPath, 'simctl', 'io', deviceId, 'screenshot', outputPath]); } } @@ -340,7 +340,7 @@ class IOSSimulator extends Device { @override Future isAppInstalled(ApplicationPackage app) { - return SimControl.instance.isInstalled(app.id); + return SimControl.instance.isInstalled(id, app.id); } @override @@ -596,7 +596,7 @@ class IOSSimulator extends Device { @override Future takeScreenshot(File outputFile) { - return SimControl.instance.takeScreenshot(outputFile.path); + return SimControl.instance.takeScreenshot(id, outputFile.path); } } diff --git a/packages/flutter_tools/test/ios/simulators_test.dart b/packages/flutter_tools/test/ios/simulators_test.dart index a1a85a63c4e4b..7bdbd5de536e1 100644 --- a/packages/flutter_tools/test/ios/simulators_test.dart +++ b/packages/flutter_tools/test/ios/simulators_test.dart @@ -161,7 +161,7 @@ void main() { '/usr/bin/xcrun', 'simctl', 'io', - 'booted', + 'x', 'screenshot', fs.path.join('some', 'path', 'to', 'screenshot.png'), ], From e56c8850ce66f4549512c61cd466f516e0ee9e4e Mon Sep 17 00:00:00 2001 From: xster Date: Thu, 15 Jun 2017 18:31:10 -0700 Subject: [PATCH 07/51] Fix simctl race (#10757) * remove usages of booted * fix --- packages/flutter_tools/lib/src/ios/simulators.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index 02ec3ddda71a5..e5f37acf37d38 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -474,7 +474,7 @@ class IOSSimulator extends Device { // Launch the updated application in the simulator. try { - SimControl.instance.launch(id, app.id, args); + await SimControl.instance.launch(id, app.id, args); } catch (error) { printError('$error'); return new LaunchResult.failed(); @@ -529,7 +529,7 @@ class IOSSimulator extends Device { throwToolExit('Could not find the built application bundle at ${bundle.path}.'); // Step 3: Install the updated bundle to the simulator. - SimControl.instance.install(id, fs.path.absolute(bundle.path)); + await SimControl.instance.install(id, fs.path.absolute(bundle.path)); } Future _sideloadUpdatedAssetsForInstalledApplicationBundle(ApplicationPackage app) => From 45446ae21b54656b68d2f7d3557add48dd97f611 Mon Sep 17 00:00:00 2001 From: xster Date: Thu, 15 Jun 2017 18:49:19 -0700 Subject: [PATCH 08/51] first round of future awaits (#10760) --- packages/flutter_tools/lib/src/version.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart index de68f74d76606..149e082f0dd26 100644 --- a/packages/flutter_tools/lib/src/version.dart +++ b/packages/flutter_tools/lib/src/version.dart @@ -209,10 +209,13 @@ class FlutterVersion { if (beenAWhileSinceWarningWasPrinted && installationSeemsOutdated && await newerFrameworkVersionAvailable()) { printStatus(versionOutOfDateMessage(frameworkAge), emphasis: true); - stamp.store( + final Future saveWarningStampFuture = stamp.store( newTimeWarningWasPrinted: _clock.now(), ); - await new Future.delayed(kPauseToLetUserReadTheMessage); + await Future.wait(>[ + saveWarningStampFuture, + new Future.delayed(kPauseToLetUserReadTheMessage), + ]); } } @@ -254,6 +257,8 @@ class FlutterVersion { try { final String branch = _channel == 'alpha' ? 'alpha' : 'master'; final DateTime remoteFrameworkCommitDate = DateTime.parse(await FlutterVersion.fetchRemoteFrameworkCommitDate(branch)); + // fire and forget since nothing subsequent depends on it. Hope there's a flush/sync mechanism. + // ignore: unawaited_futures versionCheckStamp.store( newTimeVersionWasChecked: _clock.now(), newKnownRemoteVersion: remoteFrameworkCommitDate, From d6ec71d2c06247d8ab2c247a499f64d22727269c Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Thu, 15 Jun 2017 19:03:24 -0700 Subject: [PATCH 09/51] Extract libimobiledevice tools interface (#10759) Extract out IMobileDevice class, move class to idevice_id, ideviceinfo (and eventually other libimobiledevice tools such as iproxy) behind this interface. Add tests for the case where libimobiledevice is not installed, the case where it returns no devices, and the case where it returns device IDs. --- .../flutter_tools/lib/src/ios/devices.dart | 29 ++----- .../lib/src/ios/ios_workflow.dart | 18 +---- packages/flutter_tools/lib/src/ios/mac.dart | 39 +++++++++ .../flutter_tools/test/ios/devices_test.dart | 46 +++++++++++ .../test/ios/ios_workflow_test.dart | 80 ++++++++++++++----- 5 files changed, 152 insertions(+), 60 deletions(-) diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index dc170ec94f1f0..1c11cfa244088 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -43,7 +43,6 @@ class IOSDevice extends Device { IOSDevice(String id, { this.name }) : super(id) { _installerPath = _checkForCommand('ideviceinstaller'); _listerPath = _checkForCommand('idevice_id'); - _informerPath = _checkForCommand('ideviceinfo'); _iproxyPath = _checkForCommand('iproxy'); _debuggerPath = _checkForCommand('idevicedebug'); _loggerPath = _checkForCommand('idevicesyslog'); @@ -62,9 +61,6 @@ class IOSDevice extends Device { String _listerPath; String get listerPath => _listerPath; - String _informerPath; - String get informerPath => _informerPath; - String _iproxyPath; String get iproxyPath => _iproxyPath; @@ -97,32 +93,17 @@ class IOSDevice extends Device { bool get supportsStartPaused => false; static List getAttachedDevices() { - if (!iosWorkflow.hasIDeviceId) + if (!iMobileDevice.isInstalled) return []; final List devices = []; - for (String id in _getAttachedDeviceIDs()) { - final String name = IOSDevice._getDeviceInfo(id, 'DeviceName'); + for (String id in iMobileDevice.getAttachedDeviceIDs()) { + final String name = iMobileDevice.getInfoForDevice(id, 'DeviceName'); devices.add(new IOSDevice(id, name: name)); } return devices; } - static Iterable _getAttachedDeviceIDs() { - final String listerPath = _checkForCommand('idevice_id'); - try { - final String output = runSync([listerPath, '-l']); - return output.trim().split('\n').where((String s) => s != null && s.isNotEmpty); - } catch (e) { - return []; - } - } - - static String _getDeviceInfo(String deviceID, String infoKey) { - final String informerPath = _checkForCommand('ideviceinfo'); - return runSync([informerPath, '-k', infoKey, '-u', deviceID]).trim(); - } - static String _checkForCommand( String command, [ String macInstructions = _kIdeviceinstallerInstructions @@ -353,9 +334,9 @@ class IOSDevice extends Device { @override Future get sdkNameAndVersion async => 'iOS $_sdkVersion ($_buildVersion)'; - String get _sdkVersion => _getDeviceInfo(id, 'ProductVersion'); + String get _sdkVersion => iMobileDevice.getInfoForDevice(id, 'ProductVersion'); - String get _buildVersion => _getDeviceInfo(id, 'BuildVersion'); + String get _buildVersion => iMobileDevice.getInfoForDevice(id, 'BuildVersion'); @override DeviceLogReader getLogReader({ApplicationPackage app}) { diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart index 4c640aade2d16..9f6e0de02b40b 100644 --- a/packages/flutter_tools/lib/src/ios/ios_workflow.dart +++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart @@ -7,7 +7,6 @@ import 'dart:async'; import '../base/common.dart'; import '../base/context.dart'; import '../base/file_system.dart'; -import '../base/io.dart'; import '../base/os.dart'; import '../base/platform.dart'; import '../base/process.dart'; @@ -34,21 +33,6 @@ class IOSWorkflow extends DoctorValidator implements Workflow { @override bool get canLaunchDevices => xcode.isInstalledAndMeetsVersionCheck; - bool get hasIDeviceId => exitsHappy(['idevice_id', '-h']); - - Future get hasWorkingLibimobiledevice async { - // Verify that libimobiledevice tools are installed. - if (!hasIDeviceId) - return false; - - // If a device is attached, verify that we can get its name. - final ProcessResult result = (await runAsync(['idevice_id', '-l'])).processResult; - if (result.exitCode == 0 && result.stdout.isNotEmpty && !await exitsHappyAsync(['idevicename'])) - return false; - - return true; - } - Future get hasIDeviceInstaller => exitsHappyAsync(['ideviceinstaller', '-h']); Future get hasIosDeploy => exitsHappyAsync(['ios-deploy', '--version']); @@ -154,7 +138,7 @@ class IOSWorkflow extends DoctorValidator implements Workflow { if (hasHomebrew) { brewStatus = ValidationType.installed; - if (!await hasWorkingLibimobiledevice) { + if (!await iMobileDevice.isWorking) { brewStatus = ValidationType.partial; messages.add(new ValidationMessage.error( 'libimobiledevice and ideviceinstaller are not installed or require updating. To update, run:\n' diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index ef9188af84c01..5e2337a77f9fc 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -33,6 +33,8 @@ const int kXcodeRequiredVersionMinor = 0; // Homebrew. const PythonModule kPythonSix = const PythonModule('six'); +IMobileDevice get iMobileDevice => context.putIfAbsent(IMobileDevice, () => const IMobileDevice()); + class PythonModule { const PythonModule(this.name); @@ -45,6 +47,43 @@ class PythonModule { 'Install via \'pip install $name\' or \'sudo easy_install $name\'.'; } +class IMobileDevice { + const IMobileDevice(); + + bool get isInstalled => exitsHappy(['idevice_id', '-h']); + + /// Returns true if libimobiledevice is installed and working as expected. + /// + /// Older releases of libimobiledevice fail to work with iOS 10.3 and above. + Future get isWorking async { + if (!isInstalled) + return false; + + // If no device is attached, we're unable to detect any problems. Assume all is well. + final ProcessResult result = (await runAsync(['idevice_id', '-l'])).processResult; + if (result.exitCode != 0 || result.stdout.isEmpty) + return true; + + // Check that we can look up the names of any attached devices. + return await exitsHappyAsync(['idevicename']); + } + + List getAttachedDeviceIDs() { + return runSync(['idevice_id', '-l']) + .trim() + .split('\n') + .where((String line) => line.isNotEmpty) + .toList(); + } + + /// Returns the value associated with the specified `ideviceinfo` key for a device. + /// + /// If either the specified key or device does not exist, returns the empty string. + String getInfoForDevice(String deviceID, String key) { + return runSync(['ideviceinfo', '-k', key, '-u', deviceID]).trim(); + } +} + class Xcode { Xcode() { _eulaSigned = false; diff --git a/packages/flutter_tools/test/ios/devices_test.dart b/packages/flutter_tools/test/ios/devices_test.dart index ec1b995bca500..fda5250f7a095 100644 --- a/packages/flutter_tools/test/ios/devices_test.dart +++ b/packages/flutter_tools/test/ios/devices_test.dart @@ -8,6 +8,7 @@ import 'dart:io' show ProcessResult; import 'package:file/file.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/ios/devices.dart'; +import 'package:flutter_tools/src/ios/mac.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; import 'package:process/process.dart'; @@ -16,12 +17,57 @@ import 'package:test/test.dart'; import '../src/context.dart'; class MockProcessManager extends Mock implements ProcessManager {} +class MockIMobileDevice extends Mock implements IMobileDevice {} class MockFile extends Mock implements File {} void main() { final FakePlatform osx = new FakePlatform.fromPlatform(const LocalPlatform()); osx.operatingSystem = 'macos'; + group('getAttachedDevices', () { + MockIMobileDevice mockIMobileDevice; + + setUp(() { + mockIMobileDevice = new MockIMobileDevice(); + }); + + testUsingContext('return no devices if libimobiledevice is not installed', () async { + when(mockIMobileDevice.isInstalled).thenReturn(false); + expect(IOSDevice.getAttachedDevices(), isEmpty); + }, overrides: { + IMobileDevice: () => mockIMobileDevice, + }); + + testUsingContext('returns no devices if none are attached', () async { + when(mockIMobileDevice.isInstalled).thenReturn(true); + when(mockIMobileDevice.getAttachedDeviceIDs()).thenReturn([]); + final List devices = IOSDevice.getAttachedDevices(); + expect(devices, isEmpty); + }, overrides: { + IMobileDevice: () => mockIMobileDevice, + }); + + testUsingContext('returns attached devices', () async { + when(mockIMobileDevice.isInstalled).thenReturn(true); + when(mockIMobileDevice.getAttachedDeviceIDs()).thenReturn([ + '98206e7a4afd4aedaff06e687594e089dede3c44', + 'f577a7903cc54959be2e34bc4f7f80b7009efcf4', + ]); + when(mockIMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'DeviceName')) + .thenReturn('La tele me regarde'); + when(mockIMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'DeviceName')) + .thenReturn('Puits sans fond'); + final List devices = IOSDevice.getAttachedDevices(); + expect(devices, hasLength(2)); + expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44'); + expect(devices[0].name, 'La tele me regarde'); + expect(devices[1].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4'); + expect(devices[1].name, 'Puits sans fond'); + }, overrides: { + IMobileDevice: () => mockIMobileDevice, + }); + }); + group('screenshot', () { MockProcessManager mockProcessManager; MockFile mockOutputFile; diff --git a/packages/flutter_tools/test/ios/ios_workflow_test.dart b/packages/flutter_tools/test/ios/ios_workflow_test.dart index c7afec7076802..b1142481a714e 100644 --- a/packages/flutter_tools/test/ios/ios_workflow_test.dart +++ b/packages/flutter_tools/test/ios/ios_workflow_test.dart @@ -19,11 +19,13 @@ import '../src/context.dart'; void main() { group('iOS Workflow validation', () { + MockIMobileDevice iMobileDevice; MockXcode xcode; MockProcessManager processManager; FileSystem fs; setUp(() { + iMobileDevice = new MockIMobileDevice(); xcode = new MockXcode(); processManager = new MockProcessManager(); fs = new MemoryFileSystem(); @@ -39,7 +41,10 @@ void main() { ); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.missing); - }, overrides: { Xcode: () => xcode }); + }, overrides: { + IMobileDevice: () => iMobileDevice, + Xcode: () => xcode, + }); testUsingContext('Emits partial status when Xcode is not installed', () async { when(xcode.isInstalled).thenReturn(false); @@ -47,7 +52,10 @@ void main() { final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.partial); - }, overrides: { Xcode: () => xcode }); + }, overrides: { + IMobileDevice: () => iMobileDevice, + Xcode: () => xcode, + }); testUsingContext('Emits partial status when Xcode is partially installed', () async { when(xcode.isInstalled).thenReturn(false); @@ -55,7 +63,10 @@ void main() { final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.partial); - }, overrides: { Xcode: () => xcode }); + }, overrides: { + IMobileDevice: () => iMobileDevice, + Xcode: () => xcode, + }); testUsingContext('Emits partial status when Xcode version too low', () async { when(xcode.isInstalled).thenReturn(true); @@ -66,7 +77,10 @@ void main() { final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.partial); - }, overrides: { Xcode: () => xcode }); + }, overrides: { + IMobileDevice: () => iMobileDevice, + Xcode: () => xcode, + }); testUsingContext('Emits partial status when Xcode EULA not signed', () async { when(xcode.isInstalled).thenReturn(true); @@ -77,7 +91,10 @@ void main() { final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.partial); - }, overrides: { Xcode: () => xcode }); + }, overrides: { + IMobileDevice: () => iMobileDevice, + Xcode: () => xcode, + }); testUsingContext('Emits partial status when python six not installed', () async { when(xcode.isInstalled).thenReturn(true); @@ -88,7 +105,10 @@ void main() { final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(hasPythonSixModule: false); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.partial); - }, overrides: { Xcode: () => xcode }); + }, overrides: { + IMobileDevice: () => iMobileDevice, + Xcode: () => xcode, + }); testUsingContext('Emits partial status when homebrew not installed', () async { when(xcode.isInstalled).thenReturn(true); @@ -99,7 +119,10 @@ void main() { final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(hasHomebrew: false); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.partial); - }, overrides: { Xcode: () => xcode }); + }, overrides: { + IMobileDevice: () => iMobileDevice, + Xcode: () => xcode, + }); testUsingContext('Emits partial status when libimobiledevice is not installed', () async { when(xcode.isInstalled).thenReturn(true); @@ -107,10 +130,13 @@ void main() { .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n'); when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true); when(xcode.eulaSigned).thenReturn(true); - final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(hasWorkingLibimobiledevice: false); + final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.partial); - }, overrides: { Xcode: () => xcode }); + }, overrides: { + IMobileDevice: () => new MockIMobileDevice(isWorking: false), + Xcode: () => xcode, + }); testUsingContext('Emits partial status when ios-deploy is not installed', () async { when(xcode.isInstalled).thenReturn(true); @@ -121,7 +147,10 @@ void main() { final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(hasIosDeploy: false); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.partial); - }, overrides: { Xcode: () => xcode }); + }, overrides: { + IMobileDevice: () => iMobileDevice, + Xcode: () => xcode, + }); testUsingContext('Emits partial status when ios-deploy version is too low', () async { when(xcode.isInstalled).thenReturn(true); @@ -132,7 +161,10 @@ void main() { final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(iosDeployVersionText: '1.8.0'); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.partial); - }, overrides: { Xcode: () => xcode }); + }, overrides: { + IMobileDevice: () => iMobileDevice, + Xcode: () => xcode, + }); testUsingContext('Emits partial status when CocoaPods is not installed', () async { when(xcode.isInstalled).thenReturn(true); @@ -143,7 +175,10 @@ void main() { final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(hasCocoaPods: false); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.partial); - }, overrides: { Xcode: () => xcode }); + }, overrides: { + IMobileDevice: () => iMobileDevice, + Xcode: () => xcode, + }); testUsingContext('Emits partial status when CocoaPods version is too low', () async { when(xcode.isInstalled).thenReturn(true); @@ -154,7 +189,10 @@ void main() { final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(cocoaPodsVersionText: '0.39.0'); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.partial); - }, overrides: { Xcode: () => xcode }); + }, overrides: { + IMobileDevice: () => iMobileDevice, + Xcode: () => xcode, + }); testUsingContext('Emits partial status when CocoaPods is not initialized', () async { when(xcode.isInstalled).thenReturn(true); @@ -172,6 +210,7 @@ void main() { expect(result.type, ValidationType.partial); }, overrides: { FileSystem: () => fs, + IMobileDevice: () => iMobileDevice, Xcode: () => xcode, ProcessManager: () => processManager, }); @@ -194,6 +233,7 @@ void main() { expect(result.type, ValidationType.installed); }, overrides: { FileSystem: () => fs, + IMobileDevice: () => iMobileDevice, Xcode: () => xcode, ProcessManager: () => processManager, }); @@ -207,6 +247,13 @@ final ProcessResult exitsHappy = new ProcessResult( '', // stderr ); +class MockIMobileDevice extends IMobileDevice { + MockIMobileDevice({bool isWorking: true}) : isWorking = new Future.value(isWorking); + + @override + final Future isWorking; +} + class MockXcode extends Mock implements Xcode {} class MockProcessManager extends Mock implements ProcessManager {} @@ -214,14 +261,12 @@ class IOSWorkflowTestTarget extends IOSWorkflow { IOSWorkflowTestTarget({ this.hasPythonSixModule: true, this.hasHomebrew: true, - bool hasWorkingLibimobiledevice: true, bool hasIosDeploy: true, String iosDeployVersionText: '1.9.0', bool hasIDeviceInstaller: true, bool hasCocoaPods: true, String cocoaPodsVersionText: '1.2.0', - }) : hasWorkingLibimobiledevice = new Future.value(hasWorkingLibimobiledevice), - hasIosDeploy = new Future.value(hasIosDeploy), + }) : hasIosDeploy = new Future.value(hasIosDeploy), iosDeployVersionText = new Future.value(iosDeployVersionText), hasIDeviceInstaller = new Future.value(hasIDeviceInstaller), hasCocoaPods = new Future.value(hasCocoaPods), @@ -233,9 +278,6 @@ class IOSWorkflowTestTarget extends IOSWorkflow { @override final bool hasHomebrew; - @override - final Future hasWorkingLibimobiledevice; - @override final Future hasIosDeploy; From c04f712a23854adce17c4b4af28e83d180b484af Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Thu, 15 Jun 2017 19:24:07 -0700 Subject: [PATCH 10/51] Refactor Xcode instance lookup (#10763) Use a top-level getter in mac.dart rather than a static instance getter and a top-level getter in ios_workflow.dart. Makes this code consistent with how we do context lookups elsewhere. --- packages/flutter_tools/lib/src/commands/run.dart | 2 +- packages/flutter_tools/lib/src/ios/ios_workflow.dart | 2 -- packages/flutter_tools/lib/src/ios/mac.dart | 5 ++--- packages/flutter_tools/lib/src/ios/simulators.dart | 5 ++--- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 7f84958f3a0fa..bc596fb1b05b4 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -177,7 +177,7 @@ class RunCommand extends RunCommandBase { void printNoConnectedDevices() { super.printNoConnectedDevices(); if (getCurrentHostPlatform() == HostPlatform.darwin_x64 && - Xcode.instance.isInstalledAndMeetsVersionCheck) { + xcode.isInstalledAndMeetsVersionCheck) { printStatus(''); printStatus('To run on a simulator, launch it first: open -a Simulator.app'); printStatus(''); diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart index 9f6e0de02b40b..35ee3ba7a4d9a 100644 --- a/packages/flutter_tools/lib/src/ios/ios_workflow.dart +++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart @@ -16,8 +16,6 @@ import 'mac.dart'; IOSWorkflow get iosWorkflow => context.putIfAbsent(IOSWorkflow, () => new IOSWorkflow()); -Xcode get xcode => Xcode.instance; - class IOSWorkflow extends DoctorValidator implements Workflow { IOSWorkflow() : super('iOS toolchain - develop for iOS devices'); diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 5e2337a77f9fc..a646650620426 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -35,6 +35,8 @@ const PythonModule kPythonSix = const PythonModule('six'); IMobileDevice get iMobileDevice => context.putIfAbsent(IMobileDevice, () => const IMobileDevice()); +Xcode get xcode => context.putIfAbsent(Xcode, () => new Xcode()); + class PythonModule { const PythonModule(this.name); @@ -120,9 +122,6 @@ class Xcode { } } - /// Returns [Xcode] active in the current app context. - static Xcode get instance => context.putIfAbsent(Xcode, () => new Xcode()); - bool get isInstalledAndMeetsVersionCheck => isInstalled && xcodeVersionSatisfactory; String _xcodeSelectPath; diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index e5f37acf37d38..5a57b382dbed4 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -45,7 +45,7 @@ class IOSSimulatorUtils { static IOSSimulatorUtils get instance => context[IOSSimulatorUtils]; List getAttachedDevices() { - if (!Xcode.instance.isInstalledAndMeetsVersionCheck) + if (!xcode.isInstalledAndMeetsVersionCheck) return []; return SimControl.instance.getConnectedDevices().map((SimDevice device) { @@ -587,8 +587,7 @@ class IOSSimulator extends Device { } bool get _xcodeVersionSupportsScreenshot { - return Xcode.instance.xcodeMajorVersion > 8 || - (Xcode.instance.xcodeMajorVersion == 8 && Xcode.instance.xcodeMinorVersion >= 2); + return xcode.xcodeMajorVersion > 8 || (xcode.xcodeMajorVersion == 8 && xcode.xcodeMinorVersion >= 2); } @override From 3db402ec98f964bfdba09a43c77bfd271aab611a Mon Sep 17 00:00:00 2001 From: Alexander Aprelev Date: Thu, 15 Jun 2017 23:01:25 -0700 Subject: [PATCH 11/51] Roll Flutter Engine dependency (#10753) --- bin/internal/engine.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/internal/engine.version b/bin/internal/engine.version index c63693caabebd..ac63aea80a7a7 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -18fdfb86bb3876fcbb4e1d25e5b2aad0c5cd669f +f741647d4977410ff8e2c02160eaa02bcb1f0a2b From 6965312822f5393d02efcebe060f92f3314fce42 Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Fri, 16 Jun 2017 10:05:18 -0700 Subject: [PATCH 12/51] Align doubles to 8 bytes in the StandardMessageCodec (#10758) See https://github.com/flutter/flutter/issues/10701 --- bin/internal/engine.version | 2 +- packages/flutter/lib/src/foundation/serialization.dart | 2 ++ packages/flutter/test/services/message_codecs_test.dart | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/bin/internal/engine.version b/bin/internal/engine.version index ac63aea80a7a7..7e82467506c8b 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -f741647d4977410ff8e2c02160eaa02bcb1f0a2b +784e9756720f7f6daa15c95ba3df6215bb54783f diff --git a/packages/flutter/lib/src/foundation/serialization.dart b/packages/flutter/lib/src/foundation/serialization.dart index 822d916aa0184..d43d5d8da5498 100644 --- a/packages/flutter/lib/src/foundation/serialization.dart +++ b/packages/flutter/lib/src/foundation/serialization.dart @@ -55,6 +55,7 @@ class WriteBuffer { /// Write an Float64 into the buffer. void putFloat64(double value) { + _alignTo(8); _eightBytes.setFloat64(0, value, Endianness.HOST_ENDIAN); _buffer.addAll(_eightBytesAsList); } @@ -150,6 +151,7 @@ class ReadBuffer { /// Reads a Float64 from the buffer. double getFloat64() { + _alignTo(8); final double value = data.getFloat64(_position, Endianness.HOST_ENDIAN); _position += 8; return value; diff --git a/packages/flutter/test/services/message_codecs_test.dart b/packages/flutter/test/services/message_codecs_test.dart index 176971d22d56c..ce5665231228e 100644 --- a/packages/flutter/test/services/message_codecs_test.dart +++ b/packages/flutter/test/services/message_codecs_test.dart @@ -177,6 +177,14 @@ void main() { ]; _checkEncodeDecode(standard, message); }); + test('should align doubles to 8 bytes', () { + _checkEncoding( + standard, + 1.0, + [6, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0xf0, 0x3f], + ); + }); }); } From bcac3166abd7e7aeaf2ce2070032a813c56b913a Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Fri, 16 Jun 2017 10:41:48 -0700 Subject: [PATCH 13/51] Eliminate direct invocations of idevice_id for iOS (#10777) All invocations should go via the IMobileDevice class in mac.dart. --- packages/flutter_tools/lib/src/ios/devices.dart | 4 ---- packages/flutter_tools/lib/src/ios/ios_workflow.dart | 2 +- packages/flutter_tools/test/ios/ios_workflow_test.dart | 10 ---------- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 1c11cfa244088..83b53df790162 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -42,7 +42,6 @@ class IOSDevices extends PollingDeviceDiscovery { class IOSDevice extends Device { IOSDevice(String id, { this.name }) : super(id) { _installerPath = _checkForCommand('ideviceinstaller'); - _listerPath = _checkForCommand('idevice_id'); _iproxyPath = _checkForCommand('iproxy'); _debuggerPath = _checkForCommand('idevicedebug'); _loggerPath = _checkForCommand('idevicesyslog'); @@ -58,9 +57,6 @@ class IOSDevice extends Device { String _installerPath; String get installerPath => _installerPath; - String _listerPath; - String get listerPath => _listerPath; - String _iproxyPath; String get iproxyPath => _iproxyPath; diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart index 35ee3ba7a4d9a..4e3589d1dcfc8 100644 --- a/packages/flutter_tools/lib/src/ios/ios_workflow.dart +++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart @@ -22,7 +22,7 @@ class IOSWorkflow extends DoctorValidator implements Workflow { @override bool get appliesToHostPlatform => platform.isMacOS; - // We need xcode (+simctl) to list simulator devices, and idevice_id to list real devices. + // We need xcode (+simctl) to list simulator devices, and libimobiledevice to list real devices. @override bool get canListDevices => xcode.isInstalledAndMeetsVersionCheck; diff --git a/packages/flutter_tools/test/ios/ios_workflow_test.dart b/packages/flutter_tools/test/ios/ios_workflow_test.dart index b1142481a714e..65fd9ec0a890a 100644 --- a/packages/flutter_tools/test/ios/ios_workflow_test.dart +++ b/packages/flutter_tools/test/ios/ios_workflow_test.dart @@ -201,11 +201,6 @@ void main() { when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true); when(xcode.eulaSigned).thenReturn(true); - when(processManager.runSync(argThat(contains('idevice_id')))) - .thenReturn(exitsHappy); - when(processManager.run(argThat(contains('idevice_id')), workingDirectory: any, environment: any)) - .thenReturn(exitsHappy); - final ValidationResult result = await new IOSWorkflowTestTarget().validate(); expect(result.type, ValidationType.partial); }, overrides: { @@ -222,11 +217,6 @@ void main() { when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true); when(xcode.eulaSigned).thenReturn(true); - when(processManager.runSync(argThat(contains('idevice_id')))) - .thenReturn(exitsHappy); - when(processManager.run(argThat(contains('idevice_id')), workingDirectory: any, environment: any)) - .thenReturn(exitsHappy); - ensureDirectoryExists(fs.path.join(homeDirPath, '.cocoapods', 'repos', 'master', 'README.md')); final ValidationResult result = await new IOSWorkflowTestTarget().validate(); From 73df49a0d924aa88ade81ab68cc221fe36e6e366 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Fri, 16 Jun 2017 10:42:07 -0700 Subject: [PATCH 14/51] Eliminate unused libimobiledevice tool refs (#10778) --- packages/flutter_tools/lib/src/ios/devices.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 83b53df790162..1ee72900837de 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -43,7 +43,6 @@ class IOSDevice extends Device { IOSDevice(String id, { this.name }) : super(id) { _installerPath = _checkForCommand('ideviceinstaller'); _iproxyPath = _checkForCommand('iproxy'); - _debuggerPath = _checkForCommand('idevicedebug'); _loggerPath = _checkForCommand('idevicesyslog'); _screenshotPath = _checkForCommand('idevicescreenshot'); _pusherPath = _checkForCommand( @@ -60,9 +59,6 @@ class IOSDevice extends Device { String _iproxyPath; String get iproxyPath => _iproxyPath; - String _debuggerPath; - String get debuggerPath => _debuggerPath; - String _loggerPath; String get loggerPath => _loggerPath; From aaf82b36a78fabe846d94f29656264cba6235f96 Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Fri, 16 Jun 2017 10:51:25 -0700 Subject: [PATCH 15/51] Bump version in preparation for a release of the alpha branch (#10779) --- VERSION | 2 +- packages/flutter/pubspec.yaml | 2 +- packages/flutter_driver/pubspec.yaml | 2 +- packages/flutter_test/pubspec.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index 321598098eff4..01f3057f5ba32 100644 --- a/VERSION +++ b/VERSION @@ -6,4 +6,4 @@ # incompatible way, this version number might not change. Instead, the version # number for package:flutter will update to reflect that change. -0.0.10-dev +0.0.10 diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index 095456c136a82..4c8cc7a487b86 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -1,5 +1,5 @@ name: flutter -version: 0.0.30-dev +version: 0.0.30 author: Flutter Authors description: A framework for writing Flutter applications homepage: http://flutter.io diff --git a/packages/flutter_driver/pubspec.yaml b/packages/flutter_driver/pubspec.yaml index 3f293665f6024..c06b72da4c49b 100644 --- a/packages/flutter_driver/pubspec.yaml +++ b/packages/flutter_driver/pubspec.yaml @@ -1,5 +1,5 @@ name: flutter_driver -version: 0.0.8-dev +version: 0.0.8 description: Integration and performance test API for Flutter applications homepage: http://flutter.io author: Flutter Authors diff --git a/packages/flutter_test/pubspec.yaml b/packages/flutter_test/pubspec.yaml index 42d1db7a8306d..fd088f2b7312c 100644 --- a/packages/flutter_test/pubspec.yaml +++ b/packages/flutter_test/pubspec.yaml @@ -1,5 +1,5 @@ name: flutter_test -version: 0.0.8-dev +version: 0.0.8 dependencies: # The flutter tools depend on very specific internal implementation # details of the 'test' package, which change between versions, so From b474557ef464f23703674d291d7203348f371758 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Fri, 16 Jun 2017 11:20:23 -0700 Subject: [PATCH 16/51] Eliminate unnecessary public getters for iOS tools (#10784) None of these is used outside of devices.dart and being public covers up analysis warnings when they're unused. --- .../flutter_tools/lib/src/ios/devices.dart | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 1ee72900837de..106174ce093f9 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -54,19 +54,10 @@ class IOSDevice extends Device { } String _installerPath; - String get installerPath => _installerPath; - String _iproxyPath; - String get iproxyPath => _iproxyPath; - String _loggerPath; - String get loggerPath => _loggerPath; - String _screenshotPath; - String get screenshotPath => _screenshotPath; - String _pusherPath; - String get pusherPath => _pusherPath; @override bool get supportsHotMode => true; @@ -116,7 +107,7 @@ class IOSDevice extends Device { @override Future isAppInstalled(ApplicationPackage app) async { try { - final RunResult apps = await runCheckedAsync([installerPath, '--list-apps']); + final RunResult apps = await runCheckedAsync([_installerPath, '--list-apps']); if (new RegExp(app.id, multiLine: true).hasMatch(apps.stdout)) { return true; } @@ -139,7 +130,7 @@ class IOSDevice extends Device { } try { - await runCheckedAsync([installerPath, '-i', iosApp.deviceBundlePath]); + await runCheckedAsync([_installerPath, '-i', iosApp.deviceBundlePath]); return true; } catch (e) { return false; @@ -149,7 +140,7 @@ class IOSDevice extends Device { @override Future uninstallApp(ApplicationPackage app) async { try { - await runCheckedAsync([installerPath, '-U', app.id]); + await runCheckedAsync([_installerPath, '-U', app.id]); return true; } catch (e) { return false; @@ -304,7 +295,7 @@ class IOSDevice extends Device { Future pushFile(ApplicationPackage app, String localFile, String targetFile) async { if (platform.isMacOS) { runSync([ - pusherPath, + _pusherPath, '-t', '1', '--bundle_id', @@ -344,11 +335,11 @@ class IOSDevice extends Device { } @override - bool get supportsScreenshot => screenshotPath != null && screenshotPath.isNotEmpty; + bool get supportsScreenshot => _screenshotPath != null && _screenshotPath.isNotEmpty; @override Future takeScreenshot(File outputFile) { - return runCheckedAsync([screenshotPath, outputFile.path]); + return runCheckedAsync([_screenshotPath, outputFile.path]); } } @@ -381,7 +372,7 @@ class _IOSDeviceLogReader extends DeviceLogReader { String get name => device.name; void _start() { - runCommand([device.loggerPath]).then((Process process) { + runCommand([device._loggerPath]).then((Process process) { _process = process; _process.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine); _process.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine); @@ -425,7 +416,7 @@ class _IOSDevicePortForwarder extends DevicePortForwarder { // Usage: iproxy LOCAL_TCP_PORT DEVICE_TCP_PORT UDID final Process process = await runCommand([ - device.iproxyPath, + device._iproxyPath, hostPort.toString(), devicePort.toString(), device.id, From 3b6d84b083956df5652b2fe34c68287579e9b73d Mon Sep 17 00:00:00 2001 From: Yegor Date: Fri, 16 Jun 2017 12:58:23 -0700 Subject: [PATCH 17/51] modernize iOS device lookup in driver (#10780) --- .../flutter_tools/lib/src/commands/drive.dart | 58 ++------- .../flutter_tools/lib/src/ios/simulators.dart | 113 ------------------ .../test/commands/drive_test.dart | 64 +++------- 3 files changed, 25 insertions(+), 210 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index bdc0b9f4dcf68..0bb14a0e74d75 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -4,11 +4,9 @@ import 'dart:async'; -import '../android/android_device.dart' show AndroidDevice; import '../application_package.dart'; import '../base/common.dart'; import '../base/file_system.dart'; -import '../base/platform.dart'; import '../base/process.dart'; import '../build_info.dart'; import '../cache.dart'; @@ -16,7 +14,6 @@ import '../dart/package_map.dart'; import '../dart/sdk.dart'; import '../device.dart'; import '../globals.dart'; -import '../ios/simulators.dart' show SimControl, IOSSimulatorUtils; import '../resident_runner.dart'; import 'run.dart'; @@ -198,56 +195,15 @@ Future findTargetDevice() async { return devices.first; } - - if (platform.isMacOS) { - // On Mac we look for the iOS Simulator. If available, we use that. Then - // we look for an Android device. If there's one, we use that. Otherwise, - // we launch a new iOS Simulator. - Device reusableDevice; - for (Device device in devices) { - if (await device.isLocalEmulator) { - reusableDevice = device; - break; - } - } - if (reusableDevice == null) { - for (Device device in devices) { - if (device is AndroidDevice) { - reusableDevice = device; - break; - } - } - } - - if (reusableDevice != null) { - printStatus('Found connected ${await reusableDevice.isLocalEmulator ? "emulator" : "device"} "${reusableDevice.name}"; will reuse it.'); - return reusableDevice; - } - - // No running emulator found. Attempt to start one. - printStatus('Starting iOS Simulator, because did not find existing connected devices.'); - final bool started = await SimControl.instance.boot(); - if (started) { - return IOSSimulatorUtils.instance.getAttachedDevices().first; - } else { - printError('Failed to start iOS Simulator.'); - return null; - } - } else if (platform.isLinux || platform.isWindows) { - // On Linux and Windows, for now, we just grab the first connected device we can find. - if (devices.isEmpty) { - printError('No devices found.'); - return null; - } else if (devices.length > 1) { - printStatus('Found multiple connected devices:'); - printStatus(devices.map((Device d) => ' - ${d.name}\n').join('')); - } - printStatus('Using device ${devices.first.name}.'); - return devices.first; - } else { - printError('The operating system on this computer is not supported.'); + if (devices.isEmpty) { + printError('No devices found.'); return null; + } else if (devices.length > 1) { + printStatus('Found multiple connected devices:'); + printStatus(devices.map((Device d) => ' - ${d.name}\n').join('')); } + printStatus('Using device ${devices.first.name}.'); + return devices.first; } /// Starts the application on the device given command configuration. diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index 5a57b382dbed4..697be3bac64b2 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -59,117 +59,6 @@ class SimControl { /// Returns [SimControl] active in the current app context (i.e. zone). static SimControl get instance => context[SimControl]; - Future boot({ String deviceName }) async { - if (_isAnyConnected()) - return true; - - if (deviceName == null) { - final SimDevice testDevice = _createTestDevice(); - if (testDevice == null) { - return false; - } - deviceName = testDevice.name; - } - - // `xcrun instruments` requires a template (-t). @yjbanov has no idea what - // "template" is but the built-in 'Blank' seems to work. -l causes xcrun to - // quit after a time limit without killing the simulator. We quit after - // 1 second. - final List args = [_xcrunPath, 'instruments', '-w', deviceName, '-t', 'Blank', '-l', '1']; - printTrace(args.join(' ')); - runDetached(args); - printStatus('Waiting for iOS Simulator to boot...'); - - bool connected = false; - int attempted = 0; - while (!connected && attempted < 20) { - connected = _isAnyConnected(); - if (!connected) { - printStatus('Still waiting for iOS Simulator to boot...'); - await new Future.delayed(const Duration(seconds: 1)); - } - attempted++; - } - - if (connected) { - printStatus('Connected to iOS Simulator.'); - return true; - } else { - printStatus('Timed out waiting for iOS Simulator to boot.'); - return false; - } - } - - SimDevice _createTestDevice() { - final SimDeviceType deviceType = _findSuitableDeviceType(); - if (deviceType == null) - return null; - - final String runtime = _findSuitableRuntime(); - if (runtime == null) - return null; - - // Delete any old test devices - getDevices() - .where((SimDevice d) => d.name.endsWith(_kFlutterTestDeviceSuffix)) - .forEach(_deleteDevice); - - // Create new device - final String deviceName = '${deviceType.name} $_kFlutterTestDeviceSuffix'; - final List args = [_xcrunPath, 'simctl', 'create', deviceName, deviceType.identifier, runtime]; - printTrace(args.join(' ')); - runCheckedSync(args); - - return getDevices().firstWhere((SimDevice d) => d.name == deviceName); - } - - SimDeviceType _findSuitableDeviceType() { - final List> allTypes = _list(SimControlListSection.devicetypes); - final List> usableTypes = allTypes - .where((Map info) => info['name'].startsWith('iPhone')) - .toList() - ..sort((Map r1, Map r2) => -compareIphoneVersions(r1['identifier'], r2['identifier'])); - - if (usableTypes.isEmpty) { - printError( - 'No suitable device type found.\n' - 'You may launch an iOS Simulator manually and Flutter will attempt to use it.' - ); - } - - return new SimDeviceType( - usableTypes.first['name'], - usableTypes.first['identifier'] - ); - } - - String _findSuitableRuntime() { - final List> allRuntimes = _list(SimControlListSection.runtimes); - final List> usableRuntimes = allRuntimes - .where((Map info) => info['name'].startsWith('iOS')) - .toList() - ..sort((Map r1, Map r2) => -compareIosVersions(r1['version'], r2['version'])); - - if (usableRuntimes.isEmpty) { - printError( - 'No suitable iOS runtime found.\n' - 'You may launch an iOS Simulator manually and Flutter will attempt to use it.' - ); - } - - return usableRuntimes.first['identifier']; - } - - void _deleteDevice(SimDevice device) { - try { - final List args = [_xcrunPath, 'simctl', 'delete', device.name]; - printTrace(args.join(' ')); - runCheckedSync(args); - } catch(e) { - printError(e); - } - } - /// Runs `simctl list --json` and returns the JSON of the corresponding /// [section]. /// @@ -226,8 +115,6 @@ class SimControl { return getDevices().where((SimDevice device) => device.isBooted).toList(); } - bool _isAnyConnected() => getConnectedDevices().isNotEmpty; - Future isInstalled(String deviceId, String appId) { return exitsHappyAsync([ _xcrunPath, diff --git a/packages/flutter_tools/test/commands/drive_test.dart b/packages/flutter_tools/test/commands/drive_test.dart index beef6861c54cb..aac769599d4ea 100644 --- a/packages/flutter_tools/test/commands/drive_test.dart +++ b/packages/flutter_tools/test/commands/drive_test.dart @@ -13,7 +13,6 @@ import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/drive.dart'; import 'package:flutter_tools/src/device.dart'; -import 'package:flutter_tools/src/ios/simulators.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; @@ -238,52 +237,7 @@ void main() { }); }); - group('findTargetDevice on iOS', () { - Platform macOsPlatform() => new FakePlatform(operatingSystem: 'macos'); - - testUsingContext('uses existing emulator', () async { - withMockDevice(); - when(mockDevice.name).thenReturn('mock-simulator'); - when(mockDevice.isLocalEmulator).thenReturn(new Future.value(true)); - - final Device device = await findTargetDevice(); - expect(device.name, 'mock-simulator'); - }, overrides: { - FileSystem: () => fs, - Platform: macOsPlatform, - }); - - testUsingContext('uses existing Android device if and there are no simulators', () async { - mockDevice = new MockAndroidDevice(); - when(mockDevice.name).thenReturn('mock-android-device'); - when(mockDevice.isLocalEmulator).thenReturn(new Future.value(false)); - withMockDevice(mockDevice); - - final Device device = await findTargetDevice(); - expect(device.name, 'mock-android-device'); - }, overrides: { - FileSystem: () => fs, - Platform: macOsPlatform, - }); - - testUsingContext('launches emulator', () async { - when(SimControl.instance.boot()).thenReturn(true); - final Device emulator = new MockDevice(); - when(emulator.name).thenReturn('new-simulator'); - when(IOSSimulatorUtils.instance.getAttachedDevices()) - .thenReturn([emulator]); - - final Device device = await findTargetDevice(); - expect(device.name, 'new-simulator'); - }, overrides: { - FileSystem: () => fs, - Platform: macOsPlatform, - }); - }); - void findTargetDeviceOnOperatingSystem(String operatingSystem) { - assert(operatingSystem == 'windows' || operatingSystem == 'linux'); - Platform platform() => new FakePlatform(operatingSystem: operatingSystem); testUsingContext('returns null if no devices found', () async { @@ -313,6 +267,24 @@ void main() { group('findTargetDevice on Windows', () { findTargetDeviceOnOperatingSystem('windows'); }); + + group('findTargetDevice on macOS', () { + findTargetDeviceOnOperatingSystem('macos'); + + Platform macOsPlatform() => new FakePlatform(operatingSystem: 'macos'); + + testUsingContext('uses existing simulator', () async { + withMockDevice(); + when(mockDevice.name).thenReturn('mock-simulator'); + when(mockDevice.isLocalEmulator).thenReturn(new Future.value(true)); + + final Device device = await findTargetDevice(); + expect(device.name, 'mock-simulator'); + }, overrides: { + FileSystem: () => fs, + Platform: macOsPlatform, + }); + }); }); } From 5ae6b7b97085055d935893b169b587a4babc0788 Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Fri, 16 Jun 2017 14:21:13 -0700 Subject: [PATCH 18/51] Increment version to `-dev` (#10794) --- VERSION | 2 +- packages/flutter/pubspec.yaml | 2 +- packages/flutter_driver/pubspec.yaml | 2 +- packages/flutter_test/pubspec.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index 01f3057f5ba32..706f6bb3e6aff 100644 --- a/VERSION +++ b/VERSION @@ -6,4 +6,4 @@ # incompatible way, this version number might not change. Instead, the version # number for package:flutter will update to reflect that change. -0.0.10 +0.0.11-dev diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index 4c8cc7a487b86..54df2b4f84bcf 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -1,5 +1,5 @@ name: flutter -version: 0.0.30 +version: 0.0.31-dev author: Flutter Authors description: A framework for writing Flutter applications homepage: http://flutter.io diff --git a/packages/flutter_driver/pubspec.yaml b/packages/flutter_driver/pubspec.yaml index c06b72da4c49b..21bd1a4586b39 100644 --- a/packages/flutter_driver/pubspec.yaml +++ b/packages/flutter_driver/pubspec.yaml @@ -1,5 +1,5 @@ name: flutter_driver -version: 0.0.8 +version: 0.0.9-dev description: Integration and performance test API for Flutter applications homepage: http://flutter.io author: Flutter Authors diff --git a/packages/flutter_test/pubspec.yaml b/packages/flutter_test/pubspec.yaml index fd088f2b7312c..d164f2a4ef4dc 100644 --- a/packages/flutter_test/pubspec.yaml +++ b/packages/flutter_test/pubspec.yaml @@ -1,5 +1,5 @@ name: flutter_test -version: 0.0.8 +version: 0.0.9-dev dependencies: # The flutter tools depend on very specific internal implementation # details of the 'test' package, which change between versions, so From 03393510ba0fc2035b896d701f895694ac953c69 Mon Sep 17 00:00:00 2001 From: xster Date: Fri, 16 Jun 2017 14:22:10 -0700 Subject: [PATCH 19/51] address comments (#10786) --- packages/flutter_tools/lib/src/version.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart index 149e082f0dd26..b41a77c070519 100644 --- a/packages/flutter_tools/lib/src/version.dart +++ b/packages/flutter_tools/lib/src/version.dart @@ -209,11 +209,10 @@ class FlutterVersion { if (beenAWhileSinceWarningWasPrinted && installationSeemsOutdated && await newerFrameworkVersionAvailable()) { printStatus(versionOutOfDateMessage(frameworkAge), emphasis: true); - final Future saveWarningStampFuture = stamp.store( - newTimeWarningWasPrinted: _clock.now(), - ); await Future.wait(>[ - saveWarningStampFuture, + stamp.store( + newTimeWarningWasPrinted: _clock.now(), + ), new Future.delayed(kPauseToLetUserReadTheMessage), ]); } @@ -257,9 +256,7 @@ class FlutterVersion { try { final String branch = _channel == 'alpha' ? 'alpha' : 'master'; final DateTime remoteFrameworkCommitDate = DateTime.parse(await FlutterVersion.fetchRemoteFrameworkCommitDate(branch)); - // fire and forget since nothing subsequent depends on it. Hope there's a flush/sync mechanism. - // ignore: unawaited_futures - versionCheckStamp.store( + await versionCheckStamp.store( newTimeVersionWasChecked: _clock.now(), newKnownRemoteVersion: remoteFrameworkCommitDate, ); From 66502138af7da925c9ef83003e37c24d38768164 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Fri, 16 Jun 2017 14:33:49 -0700 Subject: [PATCH 20/51] Extract all libimobiledevice invocations to IMobileDevice class (#10793) Moves all remaining calls to tools that are part of the libimobiledevice suite of tools to the IMobileDevice class. This allows for better tracking of this dependency, and easier mocking in tests. --- .../flutter_tools/lib/src/ios/devices.dart | 12 +-- packages/flutter_tools/lib/src/ios/mac.dart | 8 ++ .../flutter_tools/test/ios/devices_test.dart | 77 ------------------- packages/flutter_tools/test/ios/mac_test.dart | 56 ++++++++++++++ 4 files changed, 67 insertions(+), 86 deletions(-) diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 106174ce093f9..bb3eca7261b77 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -43,8 +43,6 @@ class IOSDevice extends Device { IOSDevice(String id, { this.name }) : super(id) { _installerPath = _checkForCommand('ideviceinstaller'); _iproxyPath = _checkForCommand('iproxy'); - _loggerPath = _checkForCommand('idevicesyslog'); - _screenshotPath = _checkForCommand('idevicescreenshot'); _pusherPath = _checkForCommand( 'ios-deploy', 'To copy files to iOS devices, please install ios-deploy. To install, run:\n' @@ -55,8 +53,6 @@ class IOSDevice extends Device { String _installerPath; String _iproxyPath; - String _loggerPath; - String _screenshotPath; String _pusherPath; @override @@ -335,12 +331,10 @@ class IOSDevice extends Device { } @override - bool get supportsScreenshot => _screenshotPath != null && _screenshotPath.isNotEmpty; + bool get supportsScreenshot => iMobileDevice.isInstalled; @override - Future takeScreenshot(File outputFile) { - return runCheckedAsync([_screenshotPath, outputFile.path]); - } + Future takeScreenshot(File outputFile) => iMobileDevice.takeScreenshot(outputFile); } class _IOSDeviceLogReader extends DeviceLogReader { @@ -372,7 +366,7 @@ class _IOSDeviceLogReader extends DeviceLogReader { String get name => device.name; void _start() { - runCommand([device._loggerPath]).then((Process process) { + iMobileDevice.startLogger().then((Process process) { _process = process; _process.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine); _process.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine); diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index a646650620426..7d1bfa6022464 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -84,6 +84,14 @@ class IMobileDevice { String getInfoForDevice(String deviceID, String key) { return runSync(['ideviceinfo', '-k', key, '-u', deviceID]).trim(); } + + /// Starts `idevicesyslog` and returns the running process. + Future startLogger() => runCommand(['idevicesyslog']); + + /// Captures a screenshot to the specified outputfile. + Future takeScreenshot(File outputFile) { + return runCheckedAsync(['idevicescreenshot', outputFile.path]); + } } class Xcode { diff --git a/packages/flutter_tools/test/ios/devices_test.dart b/packages/flutter_tools/test/ios/devices_test.dart index fda5250f7a095..4e2726874be7d 100644 --- a/packages/flutter_tools/test/ios/devices_test.dart +++ b/packages/flutter_tools/test/ios/devices_test.dart @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; -import 'dart:io' show ProcessResult; - import 'package:file/file.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/ios/devices.dart'; @@ -68,78 +65,4 @@ void main() { }); }); - group('screenshot', () { - MockProcessManager mockProcessManager; - MockFile mockOutputFile; - IOSDevice iosDeviceUnderTest; - - setUp(() { - mockProcessManager = new MockProcessManager(); - mockOutputFile = new MockFile(); - }); - - testUsingContext('error if idevicescreenshot is not installed', () async { - when(mockOutputFile.path).thenReturn(fs.path.join('some', 'test', 'path', 'image.png')); - // Let everything else return exit code 0 so process.dart doesn't crash. - // The matcher order is important. - when( - mockProcessManager.run(any, environment: null, workingDirectory: null) - ).thenReturn( - new Future.value(new ProcessResult(2, 0, '', '')) - ); - // Let `which idevicescreenshot` fail with exit code 1. - when( - mockProcessManager.runSync( - ['which', 'idevicescreenshot'], environment: null, workingDirectory: null) - ).thenReturn( - new ProcessResult(1, 1, '', '') - ); - - iosDeviceUnderTest = new IOSDevice('1234'); - await iosDeviceUnderTest.takeScreenshot(mockOutputFile); - verify(mockProcessManager.runSync( - ['which', 'idevicescreenshot'], environment: null, workingDirectory: null)); - verifyNever(mockProcessManager.run( - ['idevicescreenshot', fs.path.join('some', 'test', 'path', 'image.png')], - environment: null, - workingDirectory: null - )); - expect(testLogger.errorText, contains('brew install ideviceinstaller')); - }, - overrides: { - ProcessManager: () => mockProcessManager, - Platform: () => osx, - }); - - testUsingContext('idevicescreenshot captures and returns screenshot', () async { - when(mockOutputFile.path).thenReturn(fs.path.join('some', 'test', 'path', 'image.png')); - // Let everything else return exit code 0. - // The matcher order is important. - when( - mockProcessManager.run(any, environment: null, workingDirectory: null) - ).thenReturn( - new Future.value(new ProcessResult(4, 0, '', '')) - ); - // Let there be idevicescreenshot in the PATH. - when( - mockProcessManager.runSync( - ['which', 'idevicescreenshot'], environment: null, workingDirectory: null) - ).thenReturn( - new ProcessResult(3, 0, fs.path.join('some', 'path', 'to', 'iscreenshot'), '') - ); - - iosDeviceUnderTest = new IOSDevice('1234'); - await iosDeviceUnderTest.takeScreenshot(mockOutputFile); - verify(mockProcessManager.runSync( - ['which', 'idevicescreenshot'], environment: null, workingDirectory: null)); - verify(mockProcessManager.run( - [ - fs.path.join('some', 'path', 'to', 'iscreenshot'), - fs.path.join('some', 'test', 'path', 'image.png') - ], - environment: null, - workingDirectory: null - )); - }, overrides: {ProcessManager: () => mockProcessManager}); - }); } diff --git a/packages/flutter_tools/test/ios/mac_test.dart b/packages/flutter_tools/test/ios/mac_test.dart index e267b05ad8553..69d8dc2947b28 100644 --- a/packages/flutter_tools/test/ios/mac_test.dart +++ b/packages/flutter_tools/test/ios/mac_test.dart @@ -2,13 +2,69 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + +import 'package:file/file.dart'; import 'package:flutter_tools/src/application_package.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart' show ProcessResult; import 'package:flutter_tools/src/ios/mac.dart'; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; +import 'package:process/process.dart'; import 'package:test/test.dart'; import '../src/context.dart'; +class MockProcessManager extends Mock implements ProcessManager {} +class MockFile extends Mock implements File {} + void main() { + final FakePlatform osx = new FakePlatform.fromPlatform(const LocalPlatform()); + osx.operatingSystem = 'macos'; + + group('IMobileDevice', () { + group('screenshot', () { + final String outputPath = fs.path.join('some', 'test', 'path', 'image.png'); + MockProcessManager mockProcessManager; + MockFile mockOutputFile; + + setUp(() { + mockProcessManager = new MockProcessManager(); + mockOutputFile = new MockFile(); + }); + + testUsingContext('error if idevicescreenshot is not installed', () async { + when(mockOutputFile.path).thenReturn(outputPath); + + // Let `idevicescreenshot` fail with exit code 1. + when(mockProcessManager.run(['idevicescreenshot', outputPath], + environment: null, + workingDirectory: null + )).thenReturn(new ProcessResult(4, 1, '', '')); + + expect(() async => await iMobileDevice.takeScreenshot(mockOutputFile), throwsA(anything)); + }, overrides: { + ProcessManager: () => mockProcessManager, + Platform: () => osx, + }); + + testUsingContext('idevicescreenshot captures and returns screenshot', () async { + when(mockOutputFile.path).thenReturn(outputPath); + when(mockProcessManager.run(any, environment: null, workingDirectory: null)) + .thenReturn(new Future.value(new ProcessResult(4, 0, '', ''))); + + await iMobileDevice.takeScreenshot(mockOutputFile); + verify(mockProcessManager.run(['idevicescreenshot', outputPath], + environment: null, + workingDirectory: null + )); + }, overrides: { + ProcessManager: () => mockProcessManager, + }); + }); + }); + group('Diagnose Xcode build failure', () { BuildableIOSApp app; From 2d79ce8419db5b72a3b67e072e76edb43903d3ea Mon Sep 17 00:00:00 2001 From: Seth Ladd Date: Fri, 16 Jun 2017 14:33:59 -0700 Subject: [PATCH 21/51] don't send analytics when run from another runner (#10789) --- packages/flutter_tools/gradle/flutter.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle index da42c8d858d20..ee7113c626e25 100644 --- a/packages/flutter_tools/gradle/flutter.gradle +++ b/packages/flutter_tools/gradle/flutter.gradle @@ -110,6 +110,7 @@ class FlutterPlugin implements Plugin { if (!debugFlutterJar.isFile()) { project.exec { executable flutterExecutable.absolutePath + args "--suppress-analytics" args "precache" } if (!debugFlutterJar.isFile()) { @@ -272,6 +273,7 @@ abstract class BaseFlutterTask extends DefaultTask { args "--local-engine-src-path", localEngineSrcPath } args "build", "aot" + args "--suppress-analytics" args "--quiet" args "--target", targetPath args "--target-platform", "android-arm" @@ -288,6 +290,7 @@ abstract class BaseFlutterTask extends DefaultTask { args "--local-engine-src-path", localEngineSrcPath } args "build", "flx" + args "--suppress-analytics" args "--target", targetPath if (kernelFile != null) { args "--kernel", kernelFile.absolutePath From 5344ffc7906099b23f70bfce0a999c81a048dee8 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Fri, 16 Jun 2017 15:03:56 -0700 Subject: [PATCH 22/51] Move intrinsics tests to test mode only. (#10796) This should improve debug-time performance. --- packages/flutter/lib/src/rendering/box.dart | 6 +----- packages/flutter/lib/src/rendering/debug.dart | 13 ++++++++++--- .../flutter/lib/src/rendering/editable.dart | 11 ++++++++++- .../flutter/lib/src/rendering/paragraph.dart | 1 + .../flutter/lib/src/rendering/proxy_box.dart | 17 +++++++++++++++++ packages/flutter_test/lib/src/binding.dart | 14 +++++++++++--- 6 files changed, 50 insertions(+), 12 deletions(-) diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index 82afc4d3b14e2..52ee073c3dd4f 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -1587,9 +1587,6 @@ abstract class RenderBox extends RenderObject { @override BoxConstraints get constraints => super.constraints; - // We check the intrinsic sizes of each render box once by default. - bool _debugNeedsIntrinsicSizeCheck = true; - @override void debugAssertDoesMeetConstraints() { assert(constraints != null); @@ -1657,7 +1654,7 @@ abstract class RenderBox extends RenderObject { 'your fault. Contact support: https://github.com/flutter/flutter/issues/new' ); } - if (_debugNeedsIntrinsicSizeCheck || debugCheckIntrinsicSizes) { + if (debugCheckIntrinsicSizes) { // verify that the intrinsics are sane assert(!RenderObject.debugCheckingIntrinsics); RenderObject.debugCheckingIntrinsics = true; @@ -1696,7 +1693,6 @@ abstract class RenderBox extends RenderObject { // TODO(ianh): Test that values are internally consistent in more ways than the above. RenderObject.debugCheckingIntrinsics = false; - _debugNeedsIntrinsicSizeCheck = false; if (failures.isNotEmpty) { assert(failureCount > 0); throw new FlutterError( diff --git a/packages/flutter/lib/src/rendering/debug.dart b/packages/flutter/lib/src/rendering/debug.dart index e605d73e52bd9..54610e13ba5e7 100644 --- a/packages/flutter/lib/src/rendering/debug.dart +++ b/packages/flutter/lib/src/rendering/debug.dart @@ -106,6 +106,9 @@ bool debugPrintMarkNeedsPaintStacks = false; bool debugPrintMarkNeedsLayoutStacks = false; /// Check the intrinsic sizes of each [RenderBox] during layout. +/// +/// By default this is turned off since these checks are expensive, but it is +/// enabled by the test framework. bool debugCheckIntrinsicSizes = false; /// Adds [dart:developer.Timeline] events for every [RenderObject] painted. @@ -161,9 +164,13 @@ void debugPaintPadding(Canvas canvas, Rect outerRect, Rect innerRect, { double o /// This function is used by the test framework to ensure that debug variables /// haven't been inadvertently changed. /// -/// See [https://docs.flutter.io/flutter/rendering/rendering-library.html] for +/// See for /// a complete list. -bool debugAssertAllRenderVarsUnset(String reason) { +/// +/// The `debugCheckIntrinsicSizesOverride` argument can be provided to override +/// the expected value for [debugCheckIntrinsicSizes]. (This exists because the +/// test framework itself overrides this value in some cases.) +bool debugAssertAllRenderVarsUnset(String reason, { bool debugCheckIntrinsicSizesOverride: false }) { assert(() { if (debugPaintSizeEnabled || debugPaintBaselinesEnabled || @@ -173,7 +180,7 @@ bool debugAssertAllRenderVarsUnset(String reason) { debugRepaintTextRainbowEnabled || debugPrintMarkNeedsPaintStacks || debugPrintMarkNeedsLayoutStacks || - debugCheckIntrinsicSizes || + debugCheckIntrinsicSizes != debugCheckIntrinsicSizesOverride || debugProfilePaintsEnabled || debugPaintSizeColor != _kDebugPaintSizeColor || debugPaintSpacingColor != _kDebugPaintSpacingColor || diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index cde9cc98293d7..aa5c516b0ba6f 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -477,8 +477,17 @@ class RenderEditable extends RenderBox { _layoutText(constraints.maxWidth); _caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, _preferredLineHeight - 2.0 * _kCaretHeightOffset); _selectionRects = null; + // We grab _textPainter.size here because assigning to `size` on the next + // line will trigger us to validate our intrinsic sizes, which will change + // _textPainter's layout because the intrinsic size calculations are + // destructive, which would mean we would get different results if we later + // used properties on _textPainter in this method. + // Other _textPainter state like didExceedMaxLines will also be affected, + // though we currently don't use those here. + // See also RenderParagraph which has a similar issue. + final Size textPainterSize = _textPainter.size; size = new Size(constraints.maxWidth, constraints.constrainHeight(_preferredHeight(constraints.maxWidth))); - final Size contentSize = new Size(_textPainter.width + _kCaretGap + _kCaretWidth, _textPainter.height); + final Size contentSize = new Size(textPainterSize.width + _kCaretGap + _kCaretWidth, textPainterSize.height); final double _maxScrollExtent = _getMaxScrollExtent(contentSize); _hasVisualOverflow = _maxScrollExtent > 0.0; offset.applyViewportDimension(_viewportExtent); diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart index 5262cdd4df3b4..989eaa7a80557 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -213,6 +213,7 @@ class RenderParagraph extends RenderBox { // us to validate our intrinsic sizes, which will change _textPainter's // layout because the intrinsic size calculations are destructive. // Other _textPainter state like didExceedMaxLines will also be affected. + // See also RenderEditable which has a similar issue. final Size textSize = _textPainter.size; final bool didOverflowHeight = _textPainter.didExceedMaxLines; size = constraints.constrain(textSize); diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 86e59a3ec98dc..bb4b7e78c44af 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2862,6 +2862,23 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA break; } } + + @override + void debugFillDescription(List description) { + super.debugFillDescription(description); + final List gestures = []; + if (onTap != null) + gestures.add('tap'); + if (onLongPress != null) + gestures.add('long press'); + if (onHorizontalDragUpdate != null) + gestures.add('horizontal scroll'); + if (onVerticalDragUpdate != null) + gestures.add('vertical scroll'); + if (gestures.isEmpty) + gestures.add(''); + description.add('gestures: ${gestures.join(", ")}'); + } } /// Add annotations to the [SemanticsNode] for this subtree. diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index b200029c54ce8..a98cce8337a57 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -95,11 +95,15 @@ abstract class TestWidgetsFlutterBinding extends BindingBase /// [debugPrintOverride], which can be overridden by subclasses. TestWidgetsFlutterBinding() { debugPrint = debugPrintOverride; + debugCheckIntrinsicSizes = checkIntrinsicSizes; } @protected DebugPrintCallback get debugPrintOverride => debugPrint; + @protected + bool get checkIntrinsicSizes => false; + /// Creates and initializes the binding. This function is /// idempotent; calling it a second time will just return the /// previously-created instance. @@ -460,13 +464,14 @@ abstract class TestWidgetsFlutterBinding extends BindingBase debugPrintOverride: debugPrintOverride, )); assert(debugAssertAllRenderVarsUnset( - 'The value of a rendering debug variable was changed by the test.' + 'The value of a rendering debug variable was changed by the test.', + debugCheckIntrinsicSizesOverride: checkIntrinsicSizes, )); assert(debugAssertAllWidgetVarsUnset( - 'The value of a widget debug variable was changed by the test.' + 'The value of a widget debug variable was changed by the test.', )); assert(debugAssertAllSchedulerVarsUnset( - 'The value of a scheduler debug variable was changed by the test.' + 'The value of a scheduler debug variable was changed by the test.', )); } @@ -505,6 +510,9 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { @override DebugPrintCallback get debugPrintOverride => debugPrintSynchronously; + @override + bool get checkIntrinsicSizes => true; + @override test_package.Timeout get defaultTestTimeout => const test_package.Timeout(const Duration(seconds: 5)); From 37bb5f1300e67fe590c44bb9ecda653b2967e347 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Fri, 16 Jun 2017 16:02:28 -0700 Subject: [PATCH 23/51] Use Xcode instruments to list devices (#10801) Eliminates the dependency on idevice_id from libimobiledevice. Instead, uses Xcode built-in functionality. --- .../flutter_tools/lib/src/ios/devices.dart | 34 +++++++++++----- packages/flutter_tools/lib/src/ios/mac.dart | 10 +---- .../flutter_tools/test/ios/devices_test.dart | 40 ++++++++++--------- 3 files changed, 47 insertions(+), 37 deletions(-) diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index bb3eca7261b77..748fb7144d13a 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -40,7 +40,7 @@ class IOSDevices extends PollingDeviceDiscovery { } class IOSDevice extends Device { - IOSDevice(String id, { this.name }) : super(id) { + IOSDevice(String id, { this.name, String sdkVersion }) : _sdkVersion = sdkVersion, super(id) { _installerPath = _checkForCommand('ideviceinstaller'); _iproxyPath = _checkForCommand('iproxy'); _pusherPath = _checkForCommand( @@ -55,6 +55,8 @@ class IOSDevice extends Device { String _iproxyPath; String _pusherPath; + final String _sdkVersion; + @override bool get supportsHotMode => true; @@ -71,14 +73,30 @@ class IOSDevice extends Device { @override bool get supportsStartPaused => false; + // Physical device line format to be matched: + // My iPhone (10.3.2) [75b90e947c5f429fa67f3e9169fda0d89f0492f1] + // + // Other formats in output (desktop, simulator) to be ignored: + // my-mac-pro [2C10513E-4dA5-405C-8EF5-C44353DB3ADD] + // iPhone 6s (9.3) [F6CEE7CF-81EB-4448-81B4-1755288C7C11] (Simulator) + static final RegExp _deviceRegex = new RegExp(r'^(.*) +\((.*)\) +\[(.*)\]$'); + static List getAttachedDevices() { - if (!iMobileDevice.isInstalled) + if (!xcode.isInstalled) return []; final List devices = []; - for (String id in iMobileDevice.getAttachedDeviceIDs()) { - final String name = iMobileDevice.getInfoForDevice(id, 'DeviceName'); - devices.add(new IOSDevice(id, name: name)); + final Iterable deviceLines = xcode.getAvailableDevices() + .split('\n') + .map((String line) => line.trim()); + for (String line in deviceLines) { + final Match match = _deviceRegex.firstMatch(line); + if (match != null) { + final String deviceName = match.group(1); + final String sdkVersion = match.group(2); + final String deviceID = match.group(3); + devices.add(new IOSDevice(deviceID, name: deviceName, sdkVersion: sdkVersion)); + } } return devices; } @@ -311,11 +329,7 @@ class IOSDevice extends Device { Future get targetPlatform async => TargetPlatform.ios; @override - Future get sdkNameAndVersion async => 'iOS $_sdkVersion ($_buildVersion)'; - - String get _sdkVersion => iMobileDevice.getInfoForDevice(id, 'ProductVersion'); - - String get _buildVersion => iMobileDevice.getInfoForDevice(id, 'BuildVersion'); + Future get sdkNameAndVersion async => 'iOS $_sdkVersion'; @override DeviceLogReader getLogReader({ApplicationPackage app}) { diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 7d1bfa6022464..eabf98c4fe32c 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -70,14 +70,6 @@ class IMobileDevice { return await exitsHappyAsync(['idevicename']); } - List getAttachedDeviceIDs() { - return runSync(['idevice_id', '-l']) - .trim() - .split('\n') - .where((String line) => line.isNotEmpty) - .toList(); - } - /// Returns the value associated with the specified `ideviceinfo` key for a device. /// /// If either the specified key or device does not exist, returns the empty string. @@ -165,6 +157,8 @@ class Xcode { return _xcodeVersionCheckValid(_xcodeMajorVersion, _xcodeMinorVersion); } + + String getAvailableDevices() => runSync(['/usr/bin/instruments', '-s', 'devices']); } bool _xcodeVersionCheckValid(int major, int minor) { diff --git a/packages/flutter_tools/test/ios/devices_test.dart b/packages/flutter_tools/test/ios/devices_test.dart index 4e2726874be7d..8da07d3db78b3 100644 --- a/packages/flutter_tools/test/ios/devices_test.dart +++ b/packages/flutter_tools/test/ios/devices_test.dart @@ -14,7 +14,7 @@ import 'package:test/test.dart'; import '../src/context.dart'; class MockProcessManager extends Mock implements ProcessManager {} -class MockIMobileDevice extends Mock implements IMobileDevice {} +class MockXcode extends Mock implements Xcode {} class MockFile extends Mock implements File {} void main() { @@ -22,38 +22,40 @@ void main() { osx.operatingSystem = 'macos'; group('getAttachedDevices', () { - MockIMobileDevice mockIMobileDevice; + MockXcode mockXcode; setUp(() { - mockIMobileDevice = new MockIMobileDevice(); + mockXcode = new MockXcode(); }); - testUsingContext('return no devices if libimobiledevice is not installed', () async { - when(mockIMobileDevice.isInstalled).thenReturn(false); + testUsingContext('return no devices if Xcode is not installed', () async { + when(mockXcode.isInstalled).thenReturn(false); expect(IOSDevice.getAttachedDevices(), isEmpty); }, overrides: { - IMobileDevice: () => mockIMobileDevice, + Xcode: () => mockXcode, }); testUsingContext('returns no devices if none are attached', () async { - when(mockIMobileDevice.isInstalled).thenReturn(true); - when(mockIMobileDevice.getAttachedDeviceIDs()).thenReturn([]); + when(mockXcode.isInstalled).thenReturn(true); + when(mockXcode.getAvailableDevices()).thenReturn(''); final List devices = IOSDevice.getAttachedDevices(); expect(devices, isEmpty); }, overrides: { - IMobileDevice: () => mockIMobileDevice, + Xcode: () => mockXcode, }); testUsingContext('returns attached devices', () async { - when(mockIMobileDevice.isInstalled).thenReturn(true); - when(mockIMobileDevice.getAttachedDeviceIDs()).thenReturn([ - '98206e7a4afd4aedaff06e687594e089dede3c44', - 'f577a7903cc54959be2e34bc4f7f80b7009efcf4', - ]); - when(mockIMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'DeviceName')) - .thenReturn('La tele me regarde'); - when(mockIMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'DeviceName')) - .thenReturn('Puits sans fond'); + when(mockXcode.isInstalled).thenReturn(true); + when(mockXcode.getAvailableDevices()).thenReturn(''' +Known Devices: +je-mappelle-horse [ED6552C4-B774-5A4E-8B5A-606710C87C77] +La tele me regarde (10.3.2) [98206e7a4afd4aedaff06e687594e089dede3c44] +Puits sans fond (10.3.2) [f577a7903cc54959be2e34bc4f7f80b7009efcf4] +iPhone 6 Plus (9.3) [FBA880E6-4020-49A5-8083-DCD50CA5FA09] (Simulator) +iPhone 6s (11.0) [E805F496-FC6A-4EA4-92FF-B7901FF4E7CC] (Simulator) +iPhone 7 (11.0) + Apple Watch Series 2 - 38mm (4.0) [60027FDD-4A7A-42BF-978F-C2209D27AD61] (Simulator) +iPhone SE (11.0) [667E8DCD-5DCD-4C80-93A9-60D1D995206F] (Simulator) +'''); final List devices = IOSDevice.getAttachedDevices(); expect(devices, hasLength(2)); expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44'); @@ -61,7 +63,7 @@ void main() { expect(devices[1].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4'); expect(devices[1].name, 'Puits sans fond'); }, overrides: { - IMobileDevice: () => mockIMobileDevice, + Xcode: () => mockXcode, }); }); From 41e1eb5c64a90aaa2261b0d60f7e8e9ca0604590 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Fri, 16 Jun 2017 16:07:59 -0700 Subject: [PATCH 24/51] Various docs easy fixes (#10797) --- .../flutter/lib/src/animation/curves.dart | 2 + .../flutter/lib/src/gestures/recognizer.dart | 6 +- .../flutter/lib/src/material/ink_well.dart | 85 +++++++++++++++++-- 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/packages/flutter/lib/src/animation/curves.dart b/packages/flutter/lib/src/animation/curves.dart index f0c74528d7342..b7052d462d458 100644 --- a/packages/flutter/lib/src/animation/curves.dart +++ b/packages/flutter/lib/src/animation/curves.dart @@ -175,6 +175,8 @@ class Threshold extends Curve { /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_ease_in.png) /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_ease_out.png) /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_ease_in_out.png) +/// +/// The [Cubic] class implements third-order Bézier curves. class Cubic extends Curve { /// Creates a cubic curve. /// diff --git a/packages/flutter/lib/src/gestures/recognizer.dart b/packages/flutter/lib/src/gestures/recognizer.dart index c9107e0bda019..c7073cabc9529 100644 --- a/packages/flutter/lib/src/gestures/recognizer.dart +++ b/packages/flutter/lib/src/gestures/recognizer.dart @@ -23,11 +23,15 @@ export 'pointer_router.dart' show PointerRouter; /// anonymous functions that return objects of particular types. typedef T RecognizerCallback(); -/// The base class that all GestureRecognizers should inherit from. +/// The base class that all gesture recognizers inherit from. /// /// Provides a basic API that can be used by classes that work with /// gesture recognizers but don't care about the specific details of /// the gestures recognizers themselves. +/// +/// See also: +/// +/// * [GestureDetector], the widget that is used to detect gestures. abstract class GestureRecognizer extends GestureArenaMember { /// Registers a new pointer that might be relevant to this gesture /// detector. diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart index affb570103451..e003efed8b90c 100644 --- a/packages/flutter/lib/src/material/ink_well.dart +++ b/packages/flutter/lib/src/material/ink_well.dart @@ -21,23 +21,45 @@ import 'theme.dart'; /// For a variant of this widget that is specialized for rectangular areas that /// always clip splashes, see [InkWell]. /// +/// An [InkResponse] widget does two things when responding to a tap: +/// +/// * It starts to animate a _highlight_. The shape of the highlight is +/// determined by [highlightShape]. If it is a [BoxShape.circle], the +/// default, then the highlight is a circle of fixed size centered in the +/// [InkResponse]. If it is [BoxShape.rectangle], then the highlight is a box +/// the size of the [InkResponse] itself, unless [getRectCallback] is +/// provided, in which case that callback defines the rectangle. The color of +/// the highlight is set by [highlightColor]. +/// +/// * Simultaneously, it starts to animate a _splash_. This is a growing circle +/// initially centered on the tap location. If this is a [containedInkWell], +/// the splash grows to the [radius] while remaining centered at the tap +/// location. Otherwise, the splash migrates to the center of the box as it +/// grows. +/// /// The following two diagrams show how [InkResponse] looks when tapped if the /// [highlightShape] is [BoxShape.circle] (the default) and [containedInkWell] -/// is false (also the default). The first diagram shows how it looks if the -/// [InkResponse] is relatively large, the second shows how it looks if it is -/// small. The main thing to notice is that the splashes happily exceed the -/// bounds of the widget (because [containedInkWell] is false). +/// is false (also the default). +/// +/// The first diagram shows how it looks if the [InkResponse] is relatively +/// large: /// /// ![The highlight is a disc centered in the box, smaller than the child widget.](https://flutter.github.io/assets-for-api-docs/material/ink_response_large.png) +/// +/// The second diagram shows how it looks if the [InkResponse] is small: +/// /// ![The highlight is a disc overflowing the box, centered on the child.](https://flutter.github.io/assets-for-api-docs/material/ink_response_small.png) /// +/// The main thing to notice from these diagrams is that the splashes happily +/// exceed the bounds of the widget (because [containedInkWell] is false). +/// /// The following diagram shows the effect when the [InkResponse] has a /// [highlightShape] of [BoxShape.rectangle] with [containedInkWell] set to /// true. These are the values used by [InkWell]. /// /// ![The highlight is a rectangle the size of the box.](https://flutter.github.io/assets-for-api-docs/material/ink_well.png) /// -/// The [InkResponse] widbget must have a [Material] widget as an ancestor. The +/// The [InkResponse] widget must have a [Material] widget as an ancestor. The /// [Material] widget is where the ink reactions are actually painted. This /// matches the material design premise wherein the [Material] is what is /// actually reacting to touches by spreading ink. @@ -76,7 +98,7 @@ class InkResponse extends StatefulWidget { /// The widget below this widget in the tree. final Widget child; - /// Called when the user taps this part of the material + /// Called when the user taps this part of the material. final GestureTapCallback onTap; /// Called when the user double taps this part of the material. @@ -85,7 +107,8 @@ class InkResponse extends StatefulWidget { /// Called when the user long-presses on this part of the material. final GestureLongPressCallback onLongPress; - /// Called when this part of the material either becomes highlighted or stops behing highlighted. + /// Called when this part of the material either becomes highlighted or stops + /// being highlighted. /// /// The value passed to the callback is true if this part of the material has /// become highlighted and false if this part of the material has stopped @@ -93,12 +116,46 @@ class InkResponse extends StatefulWidget { final ValueChanged onHighlightChanged; /// Whether this ink response should be clipped its bounds. + /// + /// This flag also controls whether the splash migrates to the center of the + /// [InkResponse] or not. If [containedInkWell] is true, the splash remains + /// centered around the tap location. If it is false, the splash migrates to + /// the center of the [InkResponse] as it grows. + /// + /// See also: + /// + /// * [highlightShape], which determines the shape of the highlight. + /// * [borderRadius], which controls the corners when the box is a rectangle. + /// * [getRectCallback], which controls the size and position of the box when + /// it is a rectangle. final bool containedInkWell; - /// The shape (e.g., circle, rectangle) to use for the highlight drawn around this part of the material. + /// The shape (e.g., circle, rectangle) to use for the highlight drawn around + /// this part of the material. + /// + /// If the shape is [BoxShape.circle], then the highlight is centered on the + /// [InkResponse]. If the shape is [BoxShape.rectangle], then the highlight + /// fills the [InkResponse], or the rectangle provided by [getRectCallback] if + /// the callback is specified. + /// + /// See also: + /// + /// * [containedInkWell], which controls clipping behavior. + /// * [borderRadius], which controls the corners when the box is a rectangle. + /// * [highlightColor], the color of the highlight. + /// * [getRectCallback], which controls the size and position of the box when + /// it is a rectangle. final BoxShape highlightShape; /// The radius of the ink splash. + /// + /// Splashes grow up to this size. By default, this size is determined from + /// the size of the rectangle provided by [getRectCallback], or the size of + /// the [InkResponse] itself. + /// + /// See also: + /// + /// * [splashColor], the color of the splash. final double radius; /// The clipping radius of the containing rect. @@ -106,10 +163,20 @@ class InkResponse extends StatefulWidget { /// The highlight color of the ink response. If this property is null then the /// highlight color of the theme will be used. + /// + /// See also: + /// + /// * [highlightShape], the shape of the highlight. + /// * [splashColor], the color of the splash. final Color highlightColor; /// The splash color of the ink response. If this property is null then the /// splash color of the theme will be used. + /// + /// See also: + /// + /// * [radius], the (maximum) size of the ink splash. + /// * [highlightColor], the color of the highlight. final Color splashColor; /// The rectangle to use for the highlight effect and for clipping @@ -293,7 +360,7 @@ class _InkResponseState extends State { /// /// ![The highlight is a rectangle the size of the box.](https://flutter.github.io/assets-for-api-docs/material/ink_well.png) /// -/// The [InkResponse] widbget must have a [Material] widget as an ancestor. The +/// The [InkResponse] widget must have a [Material] widget as an ancestor. The /// [Material] widget is where the ink reactions are actually painted. This /// matches the material design premise wherein the [Material] is what is /// actually reacting to touches by spreading ink. From 972be9c8b4048e18ecfb8ab582159c8d78abace8 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Fri, 16 Jun 2017 17:47:06 -0700 Subject: [PATCH 25/51] Make device discovery asynchronous (#10803) Migrates DeviceDiscovery.devices and all device-specific lookup to be asynchronous. --- .../lib/src/android/android_device.dart | 2 +- .../lib/src/commands/daemon.dart | 37 ++++++++++--------- packages/flutter_tools/lib/src/device.dart | 26 ++++++++----- .../flutter_tools/lib/src/ios/devices.dart | 6 +-- packages/flutter_tools/lib/src/ios/mac.dart | 7 +++- .../flutter_tools/lib/src/ios/simulators.dart | 2 +- .../flutter_tools/test/ios/devices_test.dart | 14 ++++--- packages/flutter_tools/test/src/mocks.dart | 4 +- 8 files changed, 57 insertions(+), 41 deletions(-) diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index f6ed675c7ac93..8584455ec0c40 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -48,7 +48,7 @@ class AndroidDevices extends PollingDeviceDiscovery { bool get canListAnything => androidWorkflow.canListDevices; @override - List pollingGetDevices() => getAdbDevices(); + Future> pollingGetDevices() async => getAdbDevices(); } class AndroidDevice extends Device { diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 8c39078f9b596..cb069f11282a2 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -305,7 +305,7 @@ class AppDomain extends Domain { final String target = _getStringArg(args, 'target'); final bool enableHotReload = _getBoolArg(args, 'hot') ?? kHotReloadDefault; - final Device device = daemon.deviceDomain._getOrLocateDevice(deviceId); + final Device device = await daemon.deviceDomain._getOrLocateDevice(deviceId); if (device == null) throw "device '$deviceId' not found"; @@ -493,7 +493,7 @@ class AppDomain extends Domain { Future>> discover(Map args) async { final String deviceId = _getStringArg(args, 'deviceId', required: true); - final Device device = daemon.deviceDomain._getDevice(deviceId); + final Device device = await daemon.deviceDomain._getDevice(deviceId); if (device == null) throw "device '$deviceId' not found"; @@ -575,11 +575,12 @@ class DeviceDomain extends Domain { final List _discoverers = []; - Future> getDevices([Map args]) { - final List devices = _discoverers.expand((PollingDeviceDiscovery discoverer) { - return discoverer.devices; - }).toList(); - return new Future>.value(devices); + Future> getDevices([Map args]) async { + final List devices = []; + for (PollingDeviceDiscovery discoverer in _discoverers) { + devices.addAll(await discoverer.devices); + } + return devices; } /// Enable device events. @@ -602,7 +603,7 @@ class DeviceDomain extends Domain { final int devicePort = _getIntArg(args, 'devicePort', required: true); int hostPort = _getIntArg(args, 'hostPort'); - final Device device = daemon.deviceDomain._getDevice(deviceId); + final Device device = await daemon.deviceDomain._getDevice(deviceId); if (device == null) throw "device '$deviceId' not found"; @@ -617,7 +618,7 @@ class DeviceDomain extends Domain { final int devicePort = _getIntArg(args, 'devicePort', required: true); final int hostPort = _getIntArg(args, 'hostPort', required: true); - final Device device = daemon.deviceDomain._getDevice(deviceId); + final Device device = await daemon.deviceDomain._getDevice(deviceId); if (device == null) throw "device '$deviceId' not found"; @@ -631,23 +632,25 @@ class DeviceDomain extends Domain { } /// Return the device matching the deviceId field in the args. - Device _getDevice(String deviceId) { - final List devices = _discoverers.expand((PollingDeviceDiscovery discoverer) { - return discoverer.devices; - }).toList(); - return devices.firstWhere((Device device) => device.id == deviceId, orElse: () => null); + Future _getDevice(String deviceId) async { + for (PollingDeviceDiscovery discoverer in _discoverers) { + final Device device = (await discoverer.devices).firstWhere((Device device) => device.id == deviceId, orElse: () => null); + if (device != null) + return device; + } + return null; } /// Return a known matching device, or scan for devices if no known match is found. - Device _getOrLocateDevice(String deviceId) { + Future _getOrLocateDevice(String deviceId) async { // Look for an already known device. - final Device device = _getDevice(deviceId); + final Device device = await _getDevice(deviceId); if (device != null) return device; // Scan the different device providers for a match. for (PollingDeviceDiscovery discoverer in _discoverers) { - final List devices = discoverer.pollingGetDevices(); + final List devices = await discoverer.pollingGetDevices(); for (Device device in devices) if (device.id == deviceId) return device; diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index de3f873363797..8208adf9bb88e 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -81,11 +81,17 @@ class DeviceManager { : getAllConnectedDevices(); } + Iterable get _platformDiscoverers { + return _deviceDiscoverers.where((DeviceDiscovery discoverer) => discoverer.supportsPlatform); + } + /// Return the list of all connected devices. - Stream getAllConnectedDevices() { - return new Stream.fromIterable(_deviceDiscoverers - .where((DeviceDiscovery discoverer) => discoverer.supportsPlatform) - .expand((DeviceDiscovery discoverer) => discoverer.devices)); + Stream getAllConnectedDevices() async* { + for (DeviceDiscovery discoverer in _platformDiscoverers) { + for (Device device in await discoverer.devices) { + yield device; + } + } } } @@ -97,7 +103,7 @@ abstract class DeviceDiscovery { /// current environment configuration. bool get canListAnything; - List get devices; + Future> get devices; } /// A [DeviceDiscovery] implementation that uses polling to discover device adds @@ -111,13 +117,13 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { ItemListNotifier _items; Timer _timer; - List pollingGetDevices(); + Future> pollingGetDevices(); void startPolling() { if (_timer == null) { _items ??= new ItemListNotifier(); - _timer = new Timer.periodic(_pollingDuration, (Timer timer) { - _items.updateWithNewList(pollingGetDevices()); + _timer = new Timer.periodic(_pollingDuration, (Timer timer) async { + _items.updateWithNewList(await pollingGetDevices()); }); } } @@ -128,8 +134,8 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { } @override - List get devices { - _items ??= new ItemListNotifier.from(pollingGetDevices()); + Future> get devices async { + _items ??= new ItemListNotifier.from(await pollingGetDevices()); return _items.items; } diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 748fb7144d13a..1cc5c968c8daf 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -36,7 +36,7 @@ class IOSDevices extends PollingDeviceDiscovery { bool get canListAnything => iosWorkflow.canListDevices; @override - List pollingGetDevices() => IOSDevice.getAttachedDevices(); + Future> pollingGetDevices() => IOSDevice.getAttachedDevices(); } class IOSDevice extends Device { @@ -81,12 +81,12 @@ class IOSDevice extends Device { // iPhone 6s (9.3) [F6CEE7CF-81EB-4448-81B4-1755288C7C11] (Simulator) static final RegExp _deviceRegex = new RegExp(r'^(.*) +\((.*)\) +\[(.*)\]$'); - static List getAttachedDevices() { + static Future> getAttachedDevices() async { if (!xcode.isInstalled) return []; final List devices = []; - final Iterable deviceLines = xcode.getAvailableDevices() + final Iterable deviceLines = (await xcode.getAvailableDevices()) .split('\n') .map((String line) => line.trim()); for (String line in deviceLines) { diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index eabf98c4fe32c..1b1317e67eac8 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -158,7 +158,12 @@ class Xcode { return _xcodeVersionCheckValid(_xcodeMajorVersion, _xcodeMinorVersion); } - String getAvailableDevices() => runSync(['/usr/bin/instruments', '-s', 'devices']); + Future getAvailableDevices() async { + final RunResult result = await runAsync(['/usr/bin/instruments', '-s', 'devices']); + if (result.exitCode != 0) + throw new ToolExit('Failed to invoke /usr/bin/instruments. Is Xcode installed?'); + return result.stdout; + } } bool _xcodeVersionCheckValid(int major, int minor) { diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index 697be3bac64b2..92788c6e1e333 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -37,7 +37,7 @@ class IOSSimulators extends PollingDeviceDiscovery { bool get canListAnything => iosWorkflow.canListDevices; @override - List pollingGetDevices() => IOSSimulatorUtils.instance.getAttachedDevices(); + Future> pollingGetDevices() async => IOSSimulatorUtils.instance.getAttachedDevices(); } class IOSSimulatorUtils { diff --git a/packages/flutter_tools/test/ios/devices_test.dart b/packages/flutter_tools/test/ios/devices_test.dart index 8da07d3db78b3..bb29b0897faf4 100644 --- a/packages/flutter_tools/test/ios/devices_test.dart +++ b/packages/flutter_tools/test/ios/devices_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:file/file.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/ios/devices.dart'; @@ -30,15 +32,15 @@ void main() { testUsingContext('return no devices if Xcode is not installed', () async { when(mockXcode.isInstalled).thenReturn(false); - expect(IOSDevice.getAttachedDevices(), isEmpty); + expect(await IOSDevice.getAttachedDevices(), isEmpty); }, overrides: { Xcode: () => mockXcode, }); testUsingContext('returns no devices if none are attached', () async { when(mockXcode.isInstalled).thenReturn(true); - when(mockXcode.getAvailableDevices()).thenReturn(''); - final List devices = IOSDevice.getAttachedDevices(); + when(mockXcode.getAvailableDevices()).thenReturn(new Future.value('')); + final List devices = await IOSDevice.getAttachedDevices(); expect(devices, isEmpty); }, overrides: { Xcode: () => mockXcode, @@ -46,7 +48,7 @@ void main() { testUsingContext('returns attached devices', () async { when(mockXcode.isInstalled).thenReturn(true); - when(mockXcode.getAvailableDevices()).thenReturn(''' + when(mockXcode.getAvailableDevices()).thenReturn(new Future.value(''' Known Devices: je-mappelle-horse [ED6552C4-B774-5A4E-8B5A-606710C87C77] La tele me regarde (10.3.2) [98206e7a4afd4aedaff06e687594e089dede3c44] @@ -55,8 +57,8 @@ iPhone 6 Plus (9.3) [FBA880E6-4020-49A5-8083-DCD50CA5FA09] (Simulator) iPhone 6s (11.0) [E805F496-FC6A-4EA4-92FF-B7901FF4E7CC] (Simulator) iPhone 7 (11.0) + Apple Watch Series 2 - 38mm (4.0) [60027FDD-4A7A-42BF-978F-C2209D27AD61] (Simulator) iPhone SE (11.0) [667E8DCD-5DCD-4C80-93A9-60D1D995206F] (Simulator) -'''); - final List devices = IOSDevice.getAttachedDevices(); +''')); + final List devices = await IOSDevice.getAttachedDevices(); expect(devices, hasLength(2)); expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44'); expect(devices[0].name, 'La tele me regarde'); diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart index 1ac69c988bc8c..3cbfdb3f410b0 100644 --- a/packages/flutter_tools/test/src/mocks.dart +++ b/packages/flutter_tools/test/src/mocks.dart @@ -37,7 +37,7 @@ class MockPollingDeviceDiscovery extends PollingDeviceDiscovery { MockPollingDeviceDiscovery() : super('mock'); @override - List pollingGetDevices() => _devices; + Future> pollingGetDevices() async => _devices; @override bool get supportsPlatform => true; @@ -52,7 +52,7 @@ class MockPollingDeviceDiscovery extends PollingDeviceDiscovery { } @override - List get devices => _devices; + Future> get devices async => _devices; @override Stream get onAdded => _onAddedController.stream; From b2909a245a607995ce7ec286585cd1f643124f57 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Fri, 16 Jun 2017 19:00:31 -0700 Subject: [PATCH 26/51] Revert use of Xcode instruments for device lookup (#10806) * Revert "Make device discovery asynchronous (#10803)" This reverts commit 972be9c8b4048e18ecfb8ab582159c8d78abace8. * Revert "Use Xcode instruments to list devices (#10801)" This reverts commit 37bb5f1300e67fe590c44bb9ecda653b2967e347. This is to resolve a failure that looks related to a bad install of Xcode 8.0 on our build bots and should be reinstated when the infra issue is diagnosed and resolved. Instruments worked well when this was originally landed, and on the following commit, but started failing two commits after this originally landed. Manual invocation of instruments on the build host currently results in: ``` dyld: Library not loaded: @rpath/InstrumentsAnalysisCore.framework/Versions/A/InstrumentsAnalysisCore Referenced from: /Applications/Xcode8.0.app/Contents/Developer/usr/bin/instruments Reason: image not found Abort trap: 6 ``` It appears the /Applications/Xcode8.0.app/Contents/Applications directory (which contains Instruments) is missing on the host. --- .../lib/src/android/android_device.dart | 2 +- .../lib/src/commands/daemon.dart | 37 +++++++------- packages/flutter_tools/lib/src/device.dart | 26 ++++------ .../flutter_tools/lib/src/ios/devices.dart | 38 +++++---------- packages/flutter_tools/lib/src/ios/mac.dart | 15 +++--- .../flutter_tools/lib/src/ios/simulators.dart | 2 +- .../flutter_tools/test/ios/devices_test.dart | 48 +++++++++---------- packages/flutter_tools/test/src/mocks.dart | 4 +- 8 files changed, 73 insertions(+), 99 deletions(-) diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index 8584455ec0c40..f6ed675c7ac93 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -48,7 +48,7 @@ class AndroidDevices extends PollingDeviceDiscovery { bool get canListAnything => androidWorkflow.canListDevices; @override - Future> pollingGetDevices() async => getAdbDevices(); + List pollingGetDevices() => getAdbDevices(); } class AndroidDevice extends Device { diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index cb069f11282a2..8c39078f9b596 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -305,7 +305,7 @@ class AppDomain extends Domain { final String target = _getStringArg(args, 'target'); final bool enableHotReload = _getBoolArg(args, 'hot') ?? kHotReloadDefault; - final Device device = await daemon.deviceDomain._getOrLocateDevice(deviceId); + final Device device = daemon.deviceDomain._getOrLocateDevice(deviceId); if (device == null) throw "device '$deviceId' not found"; @@ -493,7 +493,7 @@ class AppDomain extends Domain { Future>> discover(Map args) async { final String deviceId = _getStringArg(args, 'deviceId', required: true); - final Device device = await daemon.deviceDomain._getDevice(deviceId); + final Device device = daemon.deviceDomain._getDevice(deviceId); if (device == null) throw "device '$deviceId' not found"; @@ -575,12 +575,11 @@ class DeviceDomain extends Domain { final List _discoverers = []; - Future> getDevices([Map args]) async { - final List devices = []; - for (PollingDeviceDiscovery discoverer in _discoverers) { - devices.addAll(await discoverer.devices); - } - return devices; + Future> getDevices([Map args]) { + final List devices = _discoverers.expand((PollingDeviceDiscovery discoverer) { + return discoverer.devices; + }).toList(); + return new Future>.value(devices); } /// Enable device events. @@ -603,7 +602,7 @@ class DeviceDomain extends Domain { final int devicePort = _getIntArg(args, 'devicePort', required: true); int hostPort = _getIntArg(args, 'hostPort'); - final Device device = await daemon.deviceDomain._getDevice(deviceId); + final Device device = daemon.deviceDomain._getDevice(deviceId); if (device == null) throw "device '$deviceId' not found"; @@ -618,7 +617,7 @@ class DeviceDomain extends Domain { final int devicePort = _getIntArg(args, 'devicePort', required: true); final int hostPort = _getIntArg(args, 'hostPort', required: true); - final Device device = await daemon.deviceDomain._getDevice(deviceId); + final Device device = daemon.deviceDomain._getDevice(deviceId); if (device == null) throw "device '$deviceId' not found"; @@ -632,25 +631,23 @@ class DeviceDomain extends Domain { } /// Return the device matching the deviceId field in the args. - Future _getDevice(String deviceId) async { - for (PollingDeviceDiscovery discoverer in _discoverers) { - final Device device = (await discoverer.devices).firstWhere((Device device) => device.id == deviceId, orElse: () => null); - if (device != null) - return device; - } - return null; + Device _getDevice(String deviceId) { + final List devices = _discoverers.expand((PollingDeviceDiscovery discoverer) { + return discoverer.devices; + }).toList(); + return devices.firstWhere((Device device) => device.id == deviceId, orElse: () => null); } /// Return a known matching device, or scan for devices if no known match is found. - Future _getOrLocateDevice(String deviceId) async { + Device _getOrLocateDevice(String deviceId) { // Look for an already known device. - final Device device = await _getDevice(deviceId); + final Device device = _getDevice(deviceId); if (device != null) return device; // Scan the different device providers for a match. for (PollingDeviceDiscovery discoverer in _discoverers) { - final List devices = await discoverer.pollingGetDevices(); + final List devices = discoverer.pollingGetDevices(); for (Device device in devices) if (device.id == deviceId) return device; diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 8208adf9bb88e..de3f873363797 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -81,17 +81,11 @@ class DeviceManager { : getAllConnectedDevices(); } - Iterable get _platformDiscoverers { - return _deviceDiscoverers.where((DeviceDiscovery discoverer) => discoverer.supportsPlatform); - } - /// Return the list of all connected devices. - Stream getAllConnectedDevices() async* { - for (DeviceDiscovery discoverer in _platformDiscoverers) { - for (Device device in await discoverer.devices) { - yield device; - } - } + Stream getAllConnectedDevices() { + return new Stream.fromIterable(_deviceDiscoverers + .where((DeviceDiscovery discoverer) => discoverer.supportsPlatform) + .expand((DeviceDiscovery discoverer) => discoverer.devices)); } } @@ -103,7 +97,7 @@ abstract class DeviceDiscovery { /// current environment configuration. bool get canListAnything; - Future> get devices; + List get devices; } /// A [DeviceDiscovery] implementation that uses polling to discover device adds @@ -117,13 +111,13 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { ItemListNotifier _items; Timer _timer; - Future> pollingGetDevices(); + List pollingGetDevices(); void startPolling() { if (_timer == null) { _items ??= new ItemListNotifier(); - _timer = new Timer.periodic(_pollingDuration, (Timer timer) async { - _items.updateWithNewList(await pollingGetDevices()); + _timer = new Timer.periodic(_pollingDuration, (Timer timer) { + _items.updateWithNewList(pollingGetDevices()); }); } } @@ -134,8 +128,8 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { } @override - Future> get devices async { - _items ??= new ItemListNotifier.from(await pollingGetDevices()); + List get devices { + _items ??= new ItemListNotifier.from(pollingGetDevices()); return _items.items; } diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 1cc5c968c8daf..bb3eca7261b77 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -36,11 +36,11 @@ class IOSDevices extends PollingDeviceDiscovery { bool get canListAnything => iosWorkflow.canListDevices; @override - Future> pollingGetDevices() => IOSDevice.getAttachedDevices(); + List pollingGetDevices() => IOSDevice.getAttachedDevices(); } class IOSDevice extends Device { - IOSDevice(String id, { this.name, String sdkVersion }) : _sdkVersion = sdkVersion, super(id) { + IOSDevice(String id, { this.name }) : super(id) { _installerPath = _checkForCommand('ideviceinstaller'); _iproxyPath = _checkForCommand('iproxy'); _pusherPath = _checkForCommand( @@ -55,8 +55,6 @@ class IOSDevice extends Device { String _iproxyPath; String _pusherPath; - final String _sdkVersion; - @override bool get supportsHotMode => true; @@ -73,30 +71,14 @@ class IOSDevice extends Device { @override bool get supportsStartPaused => false; - // Physical device line format to be matched: - // My iPhone (10.3.2) [75b90e947c5f429fa67f3e9169fda0d89f0492f1] - // - // Other formats in output (desktop, simulator) to be ignored: - // my-mac-pro [2C10513E-4dA5-405C-8EF5-C44353DB3ADD] - // iPhone 6s (9.3) [F6CEE7CF-81EB-4448-81B4-1755288C7C11] (Simulator) - static final RegExp _deviceRegex = new RegExp(r'^(.*) +\((.*)\) +\[(.*)\]$'); - - static Future> getAttachedDevices() async { - if (!xcode.isInstalled) + static List getAttachedDevices() { + if (!iMobileDevice.isInstalled) return []; final List devices = []; - final Iterable deviceLines = (await xcode.getAvailableDevices()) - .split('\n') - .map((String line) => line.trim()); - for (String line in deviceLines) { - final Match match = _deviceRegex.firstMatch(line); - if (match != null) { - final String deviceName = match.group(1); - final String sdkVersion = match.group(2); - final String deviceID = match.group(3); - devices.add(new IOSDevice(deviceID, name: deviceName, sdkVersion: sdkVersion)); - } + for (String id in iMobileDevice.getAttachedDeviceIDs()) { + final String name = iMobileDevice.getInfoForDevice(id, 'DeviceName'); + devices.add(new IOSDevice(id, name: name)); } return devices; } @@ -329,7 +311,11 @@ class IOSDevice extends Device { Future get targetPlatform async => TargetPlatform.ios; @override - Future get sdkNameAndVersion async => 'iOS $_sdkVersion'; + Future get sdkNameAndVersion async => 'iOS $_sdkVersion ($_buildVersion)'; + + String get _sdkVersion => iMobileDevice.getInfoForDevice(id, 'ProductVersion'); + + String get _buildVersion => iMobileDevice.getInfoForDevice(id, 'BuildVersion'); @override DeviceLogReader getLogReader({ApplicationPackage app}) { diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 1b1317e67eac8..7d1bfa6022464 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -70,6 +70,14 @@ class IMobileDevice { return await exitsHappyAsync(['idevicename']); } + List getAttachedDeviceIDs() { + return runSync(['idevice_id', '-l']) + .trim() + .split('\n') + .where((String line) => line.isNotEmpty) + .toList(); + } + /// Returns the value associated with the specified `ideviceinfo` key for a device. /// /// If either the specified key or device does not exist, returns the empty string. @@ -157,13 +165,6 @@ class Xcode { return _xcodeVersionCheckValid(_xcodeMajorVersion, _xcodeMinorVersion); } - - Future getAvailableDevices() async { - final RunResult result = await runAsync(['/usr/bin/instruments', '-s', 'devices']); - if (result.exitCode != 0) - throw new ToolExit('Failed to invoke /usr/bin/instruments. Is Xcode installed?'); - return result.stdout; - } } bool _xcodeVersionCheckValid(int major, int minor) { diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index 92788c6e1e333..697be3bac64b2 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -37,7 +37,7 @@ class IOSSimulators extends PollingDeviceDiscovery { bool get canListAnything => iosWorkflow.canListDevices; @override - Future> pollingGetDevices() async => IOSSimulatorUtils.instance.getAttachedDevices(); + List pollingGetDevices() => IOSSimulatorUtils.instance.getAttachedDevices(); } class IOSSimulatorUtils { diff --git a/packages/flutter_tools/test/ios/devices_test.dart b/packages/flutter_tools/test/ios/devices_test.dart index bb29b0897faf4..4e2726874be7d 100644 --- a/packages/flutter_tools/test/ios/devices_test.dart +++ b/packages/flutter_tools/test/ios/devices_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:file/file.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/ios/devices.dart'; @@ -16,7 +14,7 @@ import 'package:test/test.dart'; import '../src/context.dart'; class MockProcessManager extends Mock implements ProcessManager {} -class MockXcode extends Mock implements Xcode {} +class MockIMobileDevice extends Mock implements IMobileDevice {} class MockFile extends Mock implements File {} void main() { @@ -24,48 +22,46 @@ void main() { osx.operatingSystem = 'macos'; group('getAttachedDevices', () { - MockXcode mockXcode; + MockIMobileDevice mockIMobileDevice; setUp(() { - mockXcode = new MockXcode(); + mockIMobileDevice = new MockIMobileDevice(); }); - testUsingContext('return no devices if Xcode is not installed', () async { - when(mockXcode.isInstalled).thenReturn(false); - expect(await IOSDevice.getAttachedDevices(), isEmpty); + testUsingContext('return no devices if libimobiledevice is not installed', () async { + when(mockIMobileDevice.isInstalled).thenReturn(false); + expect(IOSDevice.getAttachedDevices(), isEmpty); }, overrides: { - Xcode: () => mockXcode, + IMobileDevice: () => mockIMobileDevice, }); testUsingContext('returns no devices if none are attached', () async { - when(mockXcode.isInstalled).thenReturn(true); - when(mockXcode.getAvailableDevices()).thenReturn(new Future.value('')); - final List devices = await IOSDevice.getAttachedDevices(); + when(mockIMobileDevice.isInstalled).thenReturn(true); + when(mockIMobileDevice.getAttachedDeviceIDs()).thenReturn([]); + final List devices = IOSDevice.getAttachedDevices(); expect(devices, isEmpty); }, overrides: { - Xcode: () => mockXcode, + IMobileDevice: () => mockIMobileDevice, }); testUsingContext('returns attached devices', () async { - when(mockXcode.isInstalled).thenReturn(true); - when(mockXcode.getAvailableDevices()).thenReturn(new Future.value(''' -Known Devices: -je-mappelle-horse [ED6552C4-B774-5A4E-8B5A-606710C87C77] -La tele me regarde (10.3.2) [98206e7a4afd4aedaff06e687594e089dede3c44] -Puits sans fond (10.3.2) [f577a7903cc54959be2e34bc4f7f80b7009efcf4] -iPhone 6 Plus (9.3) [FBA880E6-4020-49A5-8083-DCD50CA5FA09] (Simulator) -iPhone 6s (11.0) [E805F496-FC6A-4EA4-92FF-B7901FF4E7CC] (Simulator) -iPhone 7 (11.0) + Apple Watch Series 2 - 38mm (4.0) [60027FDD-4A7A-42BF-978F-C2209D27AD61] (Simulator) -iPhone SE (11.0) [667E8DCD-5DCD-4C80-93A9-60D1D995206F] (Simulator) -''')); - final List devices = await IOSDevice.getAttachedDevices(); + when(mockIMobileDevice.isInstalled).thenReturn(true); + when(mockIMobileDevice.getAttachedDeviceIDs()).thenReturn([ + '98206e7a4afd4aedaff06e687594e089dede3c44', + 'f577a7903cc54959be2e34bc4f7f80b7009efcf4', + ]); + when(mockIMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'DeviceName')) + .thenReturn('La tele me regarde'); + when(mockIMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'DeviceName')) + .thenReturn('Puits sans fond'); + final List devices = IOSDevice.getAttachedDevices(); expect(devices, hasLength(2)); expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44'); expect(devices[0].name, 'La tele me regarde'); expect(devices[1].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4'); expect(devices[1].name, 'Puits sans fond'); }, overrides: { - Xcode: () => mockXcode, + IMobileDevice: () => mockIMobileDevice, }); }); diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart index 3cbfdb3f410b0..1ac69c988bc8c 100644 --- a/packages/flutter_tools/test/src/mocks.dart +++ b/packages/flutter_tools/test/src/mocks.dart @@ -37,7 +37,7 @@ class MockPollingDeviceDiscovery extends PollingDeviceDiscovery { MockPollingDeviceDiscovery() : super('mock'); @override - Future> pollingGetDevices() async => _devices; + List pollingGetDevices() => _devices; @override bool get supportsPlatform => true; @@ -52,7 +52,7 @@ class MockPollingDeviceDiscovery extends PollingDeviceDiscovery { } @override - Future> get devices async => _devices; + List get devices => _devices; @override Stream get onAdded => _onAddedController.stream; From 28fd54c116b86405bacb3d87a8d0d69c023efcde Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Fri, 16 Jun 2017 23:54:27 -0700 Subject: [PATCH 27/51] move getTransformTo method from RenderBox to RenderObject. (#10430) * move getTransformTo method from RenderBox to RenderObject. * Cleanup comment. Add slivers test. --- packages/flutter/lib/src/rendering/box.dart | 29 --------------- .../flutter/lib/src/rendering/object.dart | 31 ++++++++++++++++ .../flutter/test/rendering/slivers_test.dart | 36 +++++++++++++++++++ 3 files changed, 67 insertions(+), 29 deletions(-) diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index 52ee073c3dd4f..065e5da6f20c2 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -11,7 +11,6 @@ import 'package:flutter/gestures.dart'; import 'package:vector_math/vector_math_64.dart'; import 'debug.dart'; -import 'node.dart'; import 'object.dart'; // This class should only be used in debug builds. @@ -1860,34 +1859,6 @@ abstract class RenderBox extends RenderObject { transform.translate(offset.dx, offset.dy); } - /// Returns a matrix that maps the local coordinate system to the coordinate - /// system of `ancestor`. - /// - /// If `ancestor` is null, this method returns a matrix that maps from the - /// local coordinate system to the coordinate system of the - /// [PipelineOwner.rootNode]. For the render tree owner by the - /// [RendererBinding] (i.e. for the main render tree displayed on the device) - /// this means that this method maps to the global coordinate system in - /// logical pixels. To get physical pixels, use [applyPaintTransform] from the - /// [RenderView] to further transform the coordinate. - Matrix4 getTransformTo(RenderObject ancestor) { - assert(attached); - if (ancestor == null) { - final AbstractNode rootNode = owner.rootNode; - if (rootNode is RenderObject) - ancestor = rootNode; - } - final List renderers = []; - for (RenderObject renderer = this; renderer != ancestor; renderer = renderer.parent) { - assert(renderer != null); // Failed to find ancestor in parent chain. - renderers.add(renderer); - } - final Matrix4 transform = new Matrix4.identity(); - for (int index = renderers.length - 1; index > 0; index -= 1) - renderers[index].applyPaintTransform(renderers[index - 1], transform); - return transform; - } - /// Convert the given point from the global coodinate system in logical pixels /// to the local coordinate system for this box. /// diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 6eb23ead8b21a..1cad998d3f7c3 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -2312,6 +2312,37 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { assert(child.parent == this); } + /// Applies the paint transform up the tree to `ancestor`. + /// + /// Returns a matrix that maps the local paint coordinate system to the + /// coordinate system of `ancestor`. + /// + /// If `ancestor` is null, this method returns a matrix that maps from the + /// local paint coordinate system to the coordinate system of the + /// [PipelineOwner.rootNode]. For the render tree owner by the + /// [RendererBinding] (i.e. for the main render tree displayed on the device) + /// this means that this method maps to the global coordinate system in + /// logical pixels. To get physical pixels, use [applyPaintTransform] from the + /// [RenderView] to further transform the coordinate. + Matrix4 getTransformTo(RenderObject ancestor) { + assert(attached); + if (ancestor == null) { + final AbstractNode rootNode = owner.rootNode; + if (rootNode is RenderObject) + ancestor = rootNode; + } + final List renderers = []; + for (RenderObject renderer = this; renderer != ancestor; renderer = renderer.parent) { + assert(renderer != null); // Failed to find ancestor in parent chain. + renderers.add(renderer); + } + final Matrix4 transform = new Matrix4.identity(); + for (int index = renderers.length - 1; index > 0; index -= 1) + renderers[index].applyPaintTransform(renderers[index - 1], transform); + return transform; + } + + /// Returns a rect in this object's coordinate system that describes /// the approximate bounding box of the clip rect that would be /// applied to the given child during the paint phase, if any. diff --git a/packages/flutter/test/rendering/slivers_test.dart b/packages/flutter/test/rendering/slivers_test.dart index bf64d5d8d12db..89215d18fdc97 100644 --- a/packages/flutter/test/rendering/slivers_test.dart +++ b/packages/flutter/test/rendering/slivers_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/rendering.dart'; import 'package:test/test.dart'; +import 'package:vector_math/vector_math_64.dart'; import 'rendering_tester.dart'; @@ -128,6 +129,11 @@ void main() { expect(result.path.first.target, equals(c)); }); + Offset _getPaintOrigin(RenderObject render) { + final Vector3 transformed3 = render.getTransformTo(null).perspectiveTransform(new Vector3(0.0, 0.0, 0.0)); + return new Offset(transformed3.x, transformed3.y); + } + test('RenderViewport basic test - right', () { RenderBox a, b, c, d, e; final RenderViewport root = new RenderViewport( @@ -146,12 +152,24 @@ void main() { expect(root.size.width, equals(800.0)); expect(root.size.height, equals(600.0)); + final RenderSliver sliverA = a.parent; + final RenderSliver sliverB = b.parent; + final RenderSliver sliverC = c.parent; + final RenderSliver sliverD = d.parent; + final RenderSliver sliverE = e.parent; + expect(a.localToGlobal(const Offset(0.0, 0.0)), const Offset(0.0, 0.0)); expect(b.localToGlobal(const Offset(0.0, 0.0)), const Offset(400.0, 0.0)); expect(c.localToGlobal(const Offset(0.0, 0.0)), const Offset(800.0, 0.0)); expect(d.localToGlobal(const Offset(0.0, 0.0)), const Offset(800.0, 0.0)); expect(e.localToGlobal(const Offset(0.0, 0.0)), const Offset(800.0, 0.0)); + expect(_getPaintOrigin(sliverA), const Offset(0.0, 0.0)); + expect(_getPaintOrigin(sliverB), const Offset(400.0, 0.0)); + expect(_getPaintOrigin(sliverC), const Offset(800.0, 0.0)); + expect(_getPaintOrigin(sliverD), const Offset(800.0, 0.0)); + expect(_getPaintOrigin(sliverE), const Offset(800.0, 0.0)); + root.offset = new ViewportOffset.fixed(200.0); pumpFrame(); expect(a.localToGlobal(const Offset(0.0, 0.0)), const Offset(-200.0, 0.0)); @@ -160,6 +178,12 @@ void main() { expect(d.localToGlobal(const Offset(0.0, 0.0)), const Offset(800.0, 0.0)); expect(e.localToGlobal(const Offset(0.0, 0.0)), const Offset(800.0, 0.0)); + expect(_getPaintOrigin(sliverA), const Offset(000.0, 0.0)); + expect(_getPaintOrigin(sliverB), const Offset(200.0, 0.0)); + expect(_getPaintOrigin(sliverC), const Offset(600.0, 0.0)); + expect(_getPaintOrigin(sliverD), const Offset(800.0, 0.0)); + expect(_getPaintOrigin(sliverE), const Offset(800.0, 0.0)); + root.offset = new ViewportOffset.fixed(600.0); pumpFrame(); expect(a.localToGlobal(const Offset(0.0, 0.0)), const Offset(-600.0, 0.0)); @@ -168,6 +192,12 @@ void main() { expect(d.localToGlobal(const Offset(0.0, 0.0)), const Offset(600.0, 0.0)); expect(e.localToGlobal(const Offset(0.0, 0.0)), const Offset(800.0, 0.0)); + expect(_getPaintOrigin(sliverA), const Offset(000.0, 0.0)); + expect(_getPaintOrigin(sliverB), const Offset(000.0, 0.0)); + expect(_getPaintOrigin(sliverC), const Offset(200.0, 0.0)); + expect(_getPaintOrigin(sliverD), const Offset(600.0, 0.0)); + expect(_getPaintOrigin(sliverE), const Offset(800.0, 0.0)); + root.offset = new ViewportOffset.fixed(900.0); pumpFrame(); expect(a.localToGlobal(const Offset(0.0, 0.0)), const Offset(-900.0, 0.0)); @@ -176,6 +206,12 @@ void main() { expect(d.localToGlobal(const Offset(0.0, 0.0)), const Offset(300.0, 0.0)); expect(e.localToGlobal(const Offset(0.0, 0.0)), const Offset(700.0, 0.0)); + expect(_getPaintOrigin(sliverA), const Offset(000.0, 0.0)); + expect(_getPaintOrigin(sliverB), const Offset(000.0, 0.0)); + expect(_getPaintOrigin(sliverC), const Offset(000.0, 0.0)); + expect(_getPaintOrigin(sliverD), const Offset(300.0, 0.0)); + expect(_getPaintOrigin(sliverE), const Offset(700.0, 0.0)); + final HitTestResult result = new HitTestResult(); root.hitTest(result, position: const Offset(150.0, 450.0)); expect(result.path.first.target, equals(c)); From 4b707c194f2706b8a9abfbf4da3b823ff30baf46 Mon Sep 17 00:00:00 2001 From: Luke Church Date: Sun, 18 Jun 2017 21:18:36 +0200 Subject: [PATCH 28/51] Change for consideration: URL for plugin text (#10720) --- packages/flutter_tools/lib/src/doctor.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index 39eba5727ebee..e9d47cb900818 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -280,8 +280,8 @@ abstract class IntelliJValidator extends DoctorValidator { if (_hasIssues(messages)) { messages.add(new ValidationMessage( - 'For information about managing plugins, see\n' - 'https://www.jetbrains.com/help/idea/managing-plugins.html' + 'For information about installing plugins, see\n' + 'https://flutter.io/intellij-setup/#installing-the-plugins' )); } From cfa0a2dbbd81ba7e9c2732da6802ce9d45727ca2 Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Mon, 19 Jun 2017 09:37:39 -0700 Subject: [PATCH 29/51] Look for APKs at the path used by Android Gradle plugin 3.0 (#10798) Fixes https://github.com/flutter/flutter/issues/10630 --- .../flutter_tools/lib/src/android/gradle.dart | 16 +++++++-- .../test/android/gradle_test.dart | 34 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 packages/flutter_tools/test/android/gradle_test.dart diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 8b4043ffcd0af..b93e9acefba16 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -211,6 +211,17 @@ Future buildGradleProjectV1(String gradle) async { printStatus('Built $gradleAppOutV1 (${getSizeAsMB(apkFile.lengthSync())}).'); } +File findApkFile(String buildDirectory, String buildModeName) { + final String apkFilename = 'app-$buildModeName.apk'; + File apkFile = fs.file('$buildDirectory/$apkFilename'); + if (apkFile.existsSync()) + return apkFile; + apkFile = fs.file('$buildDirectory/$buildModeName/$apkFilename'); + if (apkFile.existsSync()) + return apkFile; + return null; +} + Future buildGradleProjectV2(String gradle, String buildModeName, String target, String kernelPath) async { final String assembleTask = "assemble${toTitleCase(buildModeName)}"; @@ -244,8 +255,9 @@ Future buildGradleProjectV2(String gradle, String buildModeName, String ta throwToolExit('Gradle build failed: $exitcode', exitCode: exitcode); final String buildDirectory = getGradleAppOutDirV2(); - final String apkFilename = 'app-$buildModeName.apk'; - final File apkFile = fs.file('$buildDirectory/$apkFilename'); + final File apkFile = findApkFile(buildDirectory, buildModeName); + if (apkFile == null) + throwToolExit('Gradle build failed to produce an Android package.'); // Copy the APK to app.apk, so `flutter run`, `flutter install`, etc. can find it. apkFile.copySync('$buildDirectory/app.apk'); diff --git a/packages/flutter_tools/test/android/gradle_test.dart b/packages/flutter_tools/test/android/gradle_test.dart new file mode 100644 index 0000000000000..457ba8106ef29 --- /dev/null +++ b/packages/flutter_tools/test/android/gradle_test.dart @@ -0,0 +1,34 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/android/gradle.dart'; +import 'package:test/test.dart'; + +import '../src/context.dart'; + +const String _kBuildDirectory = '/build/app/outputs'; + +void main() { + FileSystem fs; + + setUp(() { + fs = new MemoryFileSystem(); + fs.directory('$_kBuildDirectory/release').createSync(recursive: true); + fs.file('$_kBuildDirectory/app-debug.apk').createSync(); + fs.file('$_kBuildDirectory/release/app-release.apk').createSync(); + }); + + group('gradle', () { + testUsingContext('findApkFile', () { + expect(findApkFile(_kBuildDirectory, 'debug').path, + '/build/app/outputs/app-debug.apk'); + expect(findApkFile(_kBuildDirectory, 'release').path, + '/build/app/outputs/release/app-release.apk'); + }, overrides: { + FileSystem: () => fs, + }); + }); +} From cf9303903c569aa2b84f0947f74e0735af0a441e Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Mon, 19 Jun 2017 09:51:36 -0700 Subject: [PATCH 30/51] Roll engine (#10808) --- bin/internal/engine.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/internal/engine.version b/bin/internal/engine.version index 7e82467506c8b..2482f6f51cc2c 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -784e9756720f7f6daa15c95ba3df6215bb54783f +105cc35af39f80218af8fb521a6dfdad9246a329 From 95eba52eeada12c78cb0be8595f08241551751b9 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Mon, 19 Jun 2017 10:37:47 -0700 Subject: [PATCH 31/51] Define some annotations for generating the widget catalog. (#10816) --- packages/flutter/lib/foundation.dart | 2 + .../lib/src/foundation/annotations.dart | 109 ++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 packages/flutter/lib/src/foundation/annotations.dart diff --git a/packages/flutter/lib/foundation.dart b/packages/flutter/lib/foundation.dart index 704a8b755f7be..31c832a061378 100644 --- a/packages/flutter/lib/foundation.dart +++ b/packages/flutter/lib/foundation.dart @@ -21,11 +21,13 @@ export 'package:meta/meta.dart' show // bool _first; // bool _lights; // bool _visible; +// class Cat { } // double _volume; // dynamic _calculation; // dynamic _last; // dynamic _selection; +export 'src/foundation/annotations.dart'; export 'src/foundation/assertions.dart'; export 'src/foundation/basic_types.dart'; export 'src/foundation/binding.dart'; diff --git a/packages/flutter/lib/src/foundation/annotations.dart b/packages/flutter/lib/src/foundation/annotations.dart new file mode 100644 index 0000000000000..a1ea49fe83e22 --- /dev/null +++ b/packages/flutter/lib/src/foundation/annotations.dart @@ -0,0 +1,109 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A category with which to annotate a class, for documentation +/// purposes. +/// +/// A category is usually represented as a section and a subsection, each +/// of which is a string. The engineering team that owns the library to which +/// the class belongs defines the categories used for classes in that library. +/// For example, the Flutter engineering team has defined categories like +/// "Basic/Buttons" and "Material Design/Buttons" for Flutter widgets. +/// +/// A class can have multiple categories. +/// +/// ## Sample code +/// +/// ```dart +/// /// A copper coffee pot, as desired by Ben Turpin. +/// /// ...documentation... +/// @Category(const ['Pots', 'Coffee']) +/// @Category(const ['Copper', 'Cookware']) +/// @DocumentationIcon('https://example.com/images/coffee.png') +/// @Summary('A proper cup of coffee is made in a proper copper coffee pot.') +/// class CopperCoffeePot { +/// // ...code... +/// } +/// ``` +/// +/// See also: +/// +/// * [DocumentationIcon], which is used to give the URL to an image that +/// represents the class. +/// * [Summary], which is used to provide a one-line description of a +/// class that overrides the inline documentations' own description. +class Category { + const Category(this.sections) : assert(sections != null); + + /// The strings the correspond to the section and subsection of the + /// category represented by this object. + /// + /// By convention, this list usually has two items. The allowed values + /// are defined by the team that owns the library to which the annotated + /// class belongs. + final List sections; +} + +/// A class annotation to provide a URL to an image that represents the class. +/// +/// Each class should only have one [DocumentationIcon]. +/// +/// ## Sample code +/// +/// ```dart +/// /// Utility class for beginning a dream-sharing sequence. +/// /// ...documentation... +/// @Category(const ['Military Technology', 'Experimental']) +/// @DocumentationIcon('https://docs.example.org/icons/top.png') +/// class DreamSharing { +/// // ...code... +/// } +/// ``` +/// +/// See also: +/// +/// * [Category], to help place the class in an index. +/// * [Summary], which is used to provide a one-line description of a +/// class that overrides the inline documentations' own description. +class DocumentationIcon { + const DocumentationIcon(this.url) : assert(url != null); + + /// The URL to an image that represents the annotated class. + final String url; +} + +/// An annotation that provides a short description of a class for use +/// in an index. +/// +/// Usually the first paragraph of the documentation for a class can be used +/// for this purpose, but on occasion the first paragraph is either too short +/// or too long for use in isolation, without the remainder of the documentation. +/// +/// ## Sample code +/// +/// ```dart +/// /// A famous cat. +/// /// +/// /// Instances of this class can hunt small animals. +/// /// This cat has three legs. +/// @Category(const ['Animals', 'Cats']) +/// @Category(const ['Cute', 'Pets']) +/// @DocumentationIcon('https://www.examples.net/docs/images/icons/pillar.jpeg') +/// @Summary('A famous three-legged cat.') +/// class Pillar extends Cat { +/// // ...code... +/// } +/// ``` +/// +/// See also: +/// +/// * [Category], to help place the class in an index. +/// * [DocumentationIcon], which is used to give the URL to an image that +/// represents the class. +class Summary { + const Summary(this.text) : assert(text != null); + + /// The text of the summary of the annotated class. + final String text; +} From f8c4a6e0d6ebd7ca2b75ccb098eea15c3f2a3ed1 Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Mon, 19 Jun 2017 11:38:42 -0700 Subject: [PATCH 32/51] Defer the animation of text fields to the caret position (#10782) Fixes https://github.com/flutter/flutter/issues/10681 --- .../lib/src/widgets/editable_text.dart | 12 +++--- .../test/material/text_field_test.dart | 38 ++++++++----------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index a4c8e7d0a7b40..eb08c06a9e50c 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -460,11 +460,13 @@ class EditableTextState extends State implements TextInputClient { // selection, then scroll the caret into view. if (_textChangedSinceLastCaretUpdate) { _textChangedSinceLastCaretUpdate = false; - _scrollController.animateTo( - _getScrollOffsetForCaret(caretRect), - curve: Curves.fastOutSlowIn, - duration: const Duration(milliseconds: 50), - ); + scheduleMicrotask(() { + _scrollController.animateTo( + _getScrollOffsetForCaret(caretRect), + curve: Curves.fastOutSlowIn, + duration: const Duration(milliseconds: 50), + ); + }); } } diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 7591c5cd70d4e..f9ea072cc7e87 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -38,6 +38,11 @@ Widget overlay(Widget child) { ); } +Future skipPastScrollingAnimation(WidgetTester tester) async { + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); +} + void main() { final MockClipboard mockClipboard = new MockClipboard(); SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall); @@ -112,8 +117,7 @@ void main() { expect(textFieldValue, equals(testValue)); await tester.pumpWidget(builder()); - // skip past scrolling animation - await tester.pump(const Duration(milliseconds: 200)); + await skipPastScrollingAnimation(tester); }); } @@ -219,8 +223,7 @@ void main() { await tester.enterText(find.byType(TextField), testValue); await tester.pumpWidget(builder()); - // skip past scrolling animation - await tester.pump(const Duration(milliseconds: 200)); + await skipPastScrollingAnimation(tester); // Tap to reposition the caret. final int tapIndex = testValue.indexOf('e'); @@ -263,8 +266,7 @@ void main() { expect(controller.value.text, testValue); await tester.pumpWidget(builder()); - // skip past scrolling animation - await tester.pump(const Duration(milliseconds: 200)); + await skipPastScrollingAnimation(tester); expect(controller.selection.isCollapsed, true); @@ -299,8 +301,7 @@ void main() { await tester.enterText(find.byType(TextField), testValue); await tester.pumpWidget(builder()); - // skip past scrolling animation - await tester.pump(const Duration(milliseconds: 200)); + await skipPastScrollingAnimation(tester); // Long press the 'e' to select 'def'. final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); @@ -362,8 +363,7 @@ void main() { final String testValue = 'abc def ghi'; await tester.enterText(find.byType(TextField), testValue); await tester.pumpWidget(builder()); - // skip past scrolling animation - await tester.pump(const Duration(milliseconds: 200)); + await skipPastScrollingAnimation(tester); // Tap the selection handle to bring up the "paste / select all" menu. await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); @@ -382,8 +382,7 @@ void main() { // COPY should reset the selection. await tester.tap(find.text('COPY')); await tester.pumpWidget(builder()); - // skip past scrolling animation - await tester.pump(const Duration(milliseconds: 200)); + await skipPastScrollingAnimation(tester); expect(controller.selection.isCollapsed, true); // Tap again to bring back the menu. @@ -418,8 +417,7 @@ void main() { final String testValue = 'abc def ghi'; await tester.enterText(find.byType(TextField), testValue); await tester.pumpWidget(builder()); - // skip past scrolling animation - await tester.pump(const Duration(milliseconds: 200)); + await skipPastScrollingAnimation(tester); // Tap the selection handle to bring up the "paste / select all" menu. await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); @@ -532,8 +530,7 @@ void main() { await tester.enterText(find.byType(TextField), testValue); await tester.pumpWidget(builder()); - // skip past scrolling animation - await tester.pump(const Duration(milliseconds: 200)); + await skipPastScrollingAnimation(tester); // Check that the text spans multiple lines. final Offset firstPos = textOffsetToPosition(tester, testValue.indexOf('First')); @@ -1340,8 +1337,7 @@ void main() { await tester.enterText(find.byType(TextField), 'a1b\n2c3'); expect(textController.text, '123'); await tester.pumpWidget(builder()); - // skip past scrolling animation - await tester.pump(const Duration(milliseconds: 200)); + await skipPastScrollingAnimation(tester); await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2'))); await tester.pumpWidget(builder()); @@ -1382,8 +1378,7 @@ void main() { final String longText = 'a' * 20; await tester.enterText(find.byType(TextField), longText); await tester.pumpWidget(builder()); - // skip past scrolling animation - await tester.pump(const Duration(milliseconds: 200)); + await skipPastScrollingAnimation(tester); ScrollableState scrollableState = tester.firstState(find.byType(Scrollable)); expect(scrollableState.position.pixels, equals(0.0)); @@ -1392,8 +1387,7 @@ void main() { // scrolls to make the caret visible. controller.selection = new TextSelection.collapsed(offset: longText.length); await tester.pumpWidget(builder()); - // skip past scrolling animation - await tester.pump(const Duration(milliseconds: 200)); + await skipPastScrollingAnimation(tester); scrollableState = tester.firstState(find.byType(Scrollable)); expect(scrollableState.position.pixels, isNot(equals(0.0))); From 1d9f00957969fcd7e8c2a323c6e44272604453fa Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Mon, 19 Jun 2017 13:14:57 -0700 Subject: [PATCH 33/51] Re-enable use of instruments for iOS device lookup (#10838) This reverts commit b2909a245a607995ce7ec286585cd1f643124f57. This resubmits the following patches: 1. Use Xcode instruments to list devices (#10801) Eliminates the dependency on idevice_id from libimobiledevice. Instead, uses Xcode built-in functionality. 2. Make device discovery asynchronous (#10803) Migrates DeviceDiscovery.devices and all device-specific lookup to be asynchronous. --- .../lib/src/android/android_device.dart | 2 +- .../lib/src/commands/daemon.dart | 37 +++++++------- packages/flutter_tools/lib/src/device.dart | 26 ++++++---- .../flutter_tools/lib/src/ios/devices.dart | 38 ++++++++++----- packages/flutter_tools/lib/src/ios/mac.dart | 15 +++--- .../flutter_tools/lib/src/ios/simulators.dart | 2 +- .../flutter_tools/test/ios/devices_test.dart | 48 ++++++++++--------- packages/flutter_tools/test/src/mocks.dart | 4 +- 8 files changed, 99 insertions(+), 73 deletions(-) diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index f6ed675c7ac93..8584455ec0c40 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -48,7 +48,7 @@ class AndroidDevices extends PollingDeviceDiscovery { bool get canListAnything => androidWorkflow.canListDevices; @override - List pollingGetDevices() => getAdbDevices(); + Future> pollingGetDevices() async => getAdbDevices(); } class AndroidDevice extends Device { diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 8c39078f9b596..cb069f11282a2 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -305,7 +305,7 @@ class AppDomain extends Domain { final String target = _getStringArg(args, 'target'); final bool enableHotReload = _getBoolArg(args, 'hot') ?? kHotReloadDefault; - final Device device = daemon.deviceDomain._getOrLocateDevice(deviceId); + final Device device = await daemon.deviceDomain._getOrLocateDevice(deviceId); if (device == null) throw "device '$deviceId' not found"; @@ -493,7 +493,7 @@ class AppDomain extends Domain { Future>> discover(Map args) async { final String deviceId = _getStringArg(args, 'deviceId', required: true); - final Device device = daemon.deviceDomain._getDevice(deviceId); + final Device device = await daemon.deviceDomain._getDevice(deviceId); if (device == null) throw "device '$deviceId' not found"; @@ -575,11 +575,12 @@ class DeviceDomain extends Domain { final List _discoverers = []; - Future> getDevices([Map args]) { - final List devices = _discoverers.expand((PollingDeviceDiscovery discoverer) { - return discoverer.devices; - }).toList(); - return new Future>.value(devices); + Future> getDevices([Map args]) async { + final List devices = []; + for (PollingDeviceDiscovery discoverer in _discoverers) { + devices.addAll(await discoverer.devices); + } + return devices; } /// Enable device events. @@ -602,7 +603,7 @@ class DeviceDomain extends Domain { final int devicePort = _getIntArg(args, 'devicePort', required: true); int hostPort = _getIntArg(args, 'hostPort'); - final Device device = daemon.deviceDomain._getDevice(deviceId); + final Device device = await daemon.deviceDomain._getDevice(deviceId); if (device == null) throw "device '$deviceId' not found"; @@ -617,7 +618,7 @@ class DeviceDomain extends Domain { final int devicePort = _getIntArg(args, 'devicePort', required: true); final int hostPort = _getIntArg(args, 'hostPort', required: true); - final Device device = daemon.deviceDomain._getDevice(deviceId); + final Device device = await daemon.deviceDomain._getDevice(deviceId); if (device == null) throw "device '$deviceId' not found"; @@ -631,23 +632,25 @@ class DeviceDomain extends Domain { } /// Return the device matching the deviceId field in the args. - Device _getDevice(String deviceId) { - final List devices = _discoverers.expand((PollingDeviceDiscovery discoverer) { - return discoverer.devices; - }).toList(); - return devices.firstWhere((Device device) => device.id == deviceId, orElse: () => null); + Future _getDevice(String deviceId) async { + for (PollingDeviceDiscovery discoverer in _discoverers) { + final Device device = (await discoverer.devices).firstWhere((Device device) => device.id == deviceId, orElse: () => null); + if (device != null) + return device; + } + return null; } /// Return a known matching device, or scan for devices if no known match is found. - Device _getOrLocateDevice(String deviceId) { + Future _getOrLocateDevice(String deviceId) async { // Look for an already known device. - final Device device = _getDevice(deviceId); + final Device device = await _getDevice(deviceId); if (device != null) return device; // Scan the different device providers for a match. for (PollingDeviceDiscovery discoverer in _discoverers) { - final List devices = discoverer.pollingGetDevices(); + final List devices = await discoverer.pollingGetDevices(); for (Device device in devices) if (device.id == deviceId) return device; diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index de3f873363797..8208adf9bb88e 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -81,11 +81,17 @@ class DeviceManager { : getAllConnectedDevices(); } + Iterable get _platformDiscoverers { + return _deviceDiscoverers.where((DeviceDiscovery discoverer) => discoverer.supportsPlatform); + } + /// Return the list of all connected devices. - Stream getAllConnectedDevices() { - return new Stream.fromIterable(_deviceDiscoverers - .where((DeviceDiscovery discoverer) => discoverer.supportsPlatform) - .expand((DeviceDiscovery discoverer) => discoverer.devices)); + Stream getAllConnectedDevices() async* { + for (DeviceDiscovery discoverer in _platformDiscoverers) { + for (Device device in await discoverer.devices) { + yield device; + } + } } } @@ -97,7 +103,7 @@ abstract class DeviceDiscovery { /// current environment configuration. bool get canListAnything; - List get devices; + Future> get devices; } /// A [DeviceDiscovery] implementation that uses polling to discover device adds @@ -111,13 +117,13 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { ItemListNotifier _items; Timer _timer; - List pollingGetDevices(); + Future> pollingGetDevices(); void startPolling() { if (_timer == null) { _items ??= new ItemListNotifier(); - _timer = new Timer.periodic(_pollingDuration, (Timer timer) { - _items.updateWithNewList(pollingGetDevices()); + _timer = new Timer.periodic(_pollingDuration, (Timer timer) async { + _items.updateWithNewList(await pollingGetDevices()); }); } } @@ -128,8 +134,8 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { } @override - List get devices { - _items ??= new ItemListNotifier.from(pollingGetDevices()); + Future> get devices async { + _items ??= new ItemListNotifier.from(await pollingGetDevices()); return _items.items; } diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index bb3eca7261b77..1cc5c968c8daf 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -36,11 +36,11 @@ class IOSDevices extends PollingDeviceDiscovery { bool get canListAnything => iosWorkflow.canListDevices; @override - List pollingGetDevices() => IOSDevice.getAttachedDevices(); + Future> pollingGetDevices() => IOSDevice.getAttachedDevices(); } class IOSDevice extends Device { - IOSDevice(String id, { this.name }) : super(id) { + IOSDevice(String id, { this.name, String sdkVersion }) : _sdkVersion = sdkVersion, super(id) { _installerPath = _checkForCommand('ideviceinstaller'); _iproxyPath = _checkForCommand('iproxy'); _pusherPath = _checkForCommand( @@ -55,6 +55,8 @@ class IOSDevice extends Device { String _iproxyPath; String _pusherPath; + final String _sdkVersion; + @override bool get supportsHotMode => true; @@ -71,14 +73,30 @@ class IOSDevice extends Device { @override bool get supportsStartPaused => false; - static List getAttachedDevices() { - if (!iMobileDevice.isInstalled) + // Physical device line format to be matched: + // My iPhone (10.3.2) [75b90e947c5f429fa67f3e9169fda0d89f0492f1] + // + // Other formats in output (desktop, simulator) to be ignored: + // my-mac-pro [2C10513E-4dA5-405C-8EF5-C44353DB3ADD] + // iPhone 6s (9.3) [F6CEE7CF-81EB-4448-81B4-1755288C7C11] (Simulator) + static final RegExp _deviceRegex = new RegExp(r'^(.*) +\((.*)\) +\[(.*)\]$'); + + static Future> getAttachedDevices() async { + if (!xcode.isInstalled) return []; final List devices = []; - for (String id in iMobileDevice.getAttachedDeviceIDs()) { - final String name = iMobileDevice.getInfoForDevice(id, 'DeviceName'); - devices.add(new IOSDevice(id, name: name)); + final Iterable deviceLines = (await xcode.getAvailableDevices()) + .split('\n') + .map((String line) => line.trim()); + for (String line in deviceLines) { + final Match match = _deviceRegex.firstMatch(line); + if (match != null) { + final String deviceName = match.group(1); + final String sdkVersion = match.group(2); + final String deviceID = match.group(3); + devices.add(new IOSDevice(deviceID, name: deviceName, sdkVersion: sdkVersion)); + } } return devices; } @@ -311,11 +329,7 @@ class IOSDevice extends Device { Future get targetPlatform async => TargetPlatform.ios; @override - Future get sdkNameAndVersion async => 'iOS $_sdkVersion ($_buildVersion)'; - - String get _sdkVersion => iMobileDevice.getInfoForDevice(id, 'ProductVersion'); - - String get _buildVersion => iMobileDevice.getInfoForDevice(id, 'BuildVersion'); + Future get sdkNameAndVersion async => 'iOS $_sdkVersion'; @override DeviceLogReader getLogReader({ApplicationPackage app}) { diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 7d1bfa6022464..1b1317e67eac8 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -70,14 +70,6 @@ class IMobileDevice { return await exitsHappyAsync(['idevicename']); } - List getAttachedDeviceIDs() { - return runSync(['idevice_id', '-l']) - .trim() - .split('\n') - .where((String line) => line.isNotEmpty) - .toList(); - } - /// Returns the value associated with the specified `ideviceinfo` key for a device. /// /// If either the specified key or device does not exist, returns the empty string. @@ -165,6 +157,13 @@ class Xcode { return _xcodeVersionCheckValid(_xcodeMajorVersion, _xcodeMinorVersion); } + + Future getAvailableDevices() async { + final RunResult result = await runAsync(['/usr/bin/instruments', '-s', 'devices']); + if (result.exitCode != 0) + throw new ToolExit('Failed to invoke /usr/bin/instruments. Is Xcode installed?'); + return result.stdout; + } } bool _xcodeVersionCheckValid(int major, int minor) { diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index 697be3bac64b2..92788c6e1e333 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -37,7 +37,7 @@ class IOSSimulators extends PollingDeviceDiscovery { bool get canListAnything => iosWorkflow.canListDevices; @override - List pollingGetDevices() => IOSSimulatorUtils.instance.getAttachedDevices(); + Future> pollingGetDevices() async => IOSSimulatorUtils.instance.getAttachedDevices(); } class IOSSimulatorUtils { diff --git a/packages/flutter_tools/test/ios/devices_test.dart b/packages/flutter_tools/test/ios/devices_test.dart index 4e2726874be7d..bb29b0897faf4 100644 --- a/packages/flutter_tools/test/ios/devices_test.dart +++ b/packages/flutter_tools/test/ios/devices_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:file/file.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/ios/devices.dart'; @@ -14,7 +16,7 @@ import 'package:test/test.dart'; import '../src/context.dart'; class MockProcessManager extends Mock implements ProcessManager {} -class MockIMobileDevice extends Mock implements IMobileDevice {} +class MockXcode extends Mock implements Xcode {} class MockFile extends Mock implements File {} void main() { @@ -22,46 +24,48 @@ void main() { osx.operatingSystem = 'macos'; group('getAttachedDevices', () { - MockIMobileDevice mockIMobileDevice; + MockXcode mockXcode; setUp(() { - mockIMobileDevice = new MockIMobileDevice(); + mockXcode = new MockXcode(); }); - testUsingContext('return no devices if libimobiledevice is not installed', () async { - when(mockIMobileDevice.isInstalled).thenReturn(false); - expect(IOSDevice.getAttachedDevices(), isEmpty); + testUsingContext('return no devices if Xcode is not installed', () async { + when(mockXcode.isInstalled).thenReturn(false); + expect(await IOSDevice.getAttachedDevices(), isEmpty); }, overrides: { - IMobileDevice: () => mockIMobileDevice, + Xcode: () => mockXcode, }); testUsingContext('returns no devices if none are attached', () async { - when(mockIMobileDevice.isInstalled).thenReturn(true); - when(mockIMobileDevice.getAttachedDeviceIDs()).thenReturn([]); - final List devices = IOSDevice.getAttachedDevices(); + when(mockXcode.isInstalled).thenReturn(true); + when(mockXcode.getAvailableDevices()).thenReturn(new Future.value('')); + final List devices = await IOSDevice.getAttachedDevices(); expect(devices, isEmpty); }, overrides: { - IMobileDevice: () => mockIMobileDevice, + Xcode: () => mockXcode, }); testUsingContext('returns attached devices', () async { - when(mockIMobileDevice.isInstalled).thenReturn(true); - when(mockIMobileDevice.getAttachedDeviceIDs()).thenReturn([ - '98206e7a4afd4aedaff06e687594e089dede3c44', - 'f577a7903cc54959be2e34bc4f7f80b7009efcf4', - ]); - when(mockIMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'DeviceName')) - .thenReturn('La tele me regarde'); - when(mockIMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'DeviceName')) - .thenReturn('Puits sans fond'); - final List devices = IOSDevice.getAttachedDevices(); + when(mockXcode.isInstalled).thenReturn(true); + when(mockXcode.getAvailableDevices()).thenReturn(new Future.value(''' +Known Devices: +je-mappelle-horse [ED6552C4-B774-5A4E-8B5A-606710C87C77] +La tele me regarde (10.3.2) [98206e7a4afd4aedaff06e687594e089dede3c44] +Puits sans fond (10.3.2) [f577a7903cc54959be2e34bc4f7f80b7009efcf4] +iPhone 6 Plus (9.3) [FBA880E6-4020-49A5-8083-DCD50CA5FA09] (Simulator) +iPhone 6s (11.0) [E805F496-FC6A-4EA4-92FF-B7901FF4E7CC] (Simulator) +iPhone 7 (11.0) + Apple Watch Series 2 - 38mm (4.0) [60027FDD-4A7A-42BF-978F-C2209D27AD61] (Simulator) +iPhone SE (11.0) [667E8DCD-5DCD-4C80-93A9-60D1D995206F] (Simulator) +''')); + final List devices = await IOSDevice.getAttachedDevices(); expect(devices, hasLength(2)); expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44'); expect(devices[0].name, 'La tele me regarde'); expect(devices[1].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4'); expect(devices[1].name, 'Puits sans fond'); }, overrides: { - IMobileDevice: () => mockIMobileDevice, + Xcode: () => mockXcode, }); }); diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart index 1ac69c988bc8c..3cbfdb3f410b0 100644 --- a/packages/flutter_tools/test/src/mocks.dart +++ b/packages/flutter_tools/test/src/mocks.dart @@ -37,7 +37,7 @@ class MockPollingDeviceDiscovery extends PollingDeviceDiscovery { MockPollingDeviceDiscovery() : super('mock'); @override - List pollingGetDevices() => _devices; + Future> pollingGetDevices() async => _devices; @override bool get supportsPlatform => true; @@ -52,7 +52,7 @@ class MockPollingDeviceDiscovery extends PollingDeviceDiscovery { } @override - List get devices => _devices; + Future> get devices async => _devices; @override Stream get onAdded => _onAddedController.stream; From 400a62d12177008c73c61fd33f9f9ecf8ea2b6db Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Mon, 19 Jun 2017 13:36:05 -0700 Subject: [PATCH 34/51] Eliminate use of ideviceinfo in flutter_tools (#10804) This libimobiledevice tool is no longer used anywhere in the flutter_tools codebase. --- packages/flutter_tools/lib/src/ios/mac.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 1b1317e67eac8..c13e5e1acb54a 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -70,13 +70,6 @@ class IMobileDevice { return await exitsHappyAsync(['idevicename']); } - /// Returns the value associated with the specified `ideviceinfo` key for a device. - /// - /// If either the specified key or device does not exist, returns the empty string. - String getInfoForDevice(String deviceID, String key) { - return runSync(['ideviceinfo', '-k', key, '-u', deviceID]).trim(); - } - /// Starts `idevicesyslog` and returns the running process. Future startLogger() => runCommand(['idevicesyslog']); From 58fe8237d2ec012ea51a673bc228f6545af8bb3c Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Mon, 19 Jun 2017 14:44:42 -0700 Subject: [PATCH 35/51] Use Xcode instruments for devicelab device lookup (#10840) Replace use of ideviceinfo in devicelab tests with Xcode instruments lookup. --- dev/devicelab/lib/framework/adb.dart | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/dev/devicelab/lib/framework/adb.dart b/dev/devicelab/lib/framework/adb.dart index 042f591872f67..ee6281af7660e 100644 --- a/dev/devicelab/lib/framework/adb.dart +++ b/dev/devicelab/lib/framework/adb.dart @@ -306,16 +306,31 @@ class IosDeviceDiscovery implements DeviceDiscovery { _workingDevice = allDevices[new math.Random().nextInt(allDevices.length)]; } + // Physical device line format to be matched: + // My iPhone (10.3.2) [75b90e947c5f429fa67f3e9169fda0d89f0492f1] + // + // Other formats in output (desktop, simulator) to be ignored: + // my-mac-pro [2C10513E-4dA5-405C-8EF5-C44353DB3ADD] + // iPhone 6s (9.3) [F6CEE7CF-81EB-4448-81B4-1755288C7C11] (Simulator) + static final RegExp _deviceRegex = new RegExp(r'^.* +\(.*\) +\[(.*)\]$'); + @override Future> discoverDevices() async { - // TODO: use the -k UniqueDeviceID option, which requires much less parsing. - final List iosDeviceIds = grep('UniqueDeviceID', from: await eval('ideviceinfo', [])) - .map((String line) => line.split(' ').last).toList(); - - if (iosDeviceIds.isEmpty) + final List iosDeviceIDs = []; + final Iterable deviceLines = (await eval('instruments', ['-s', 'devices'])) + .split('\n') + .map((String line) => line.trim()); + for (String line in deviceLines) { + final Match match = _deviceRegex.firstMatch(line); + if (match != null) { + final String deviceID = match.group(1); + iosDeviceIDs.add(deviceID); + } + } + if (iosDeviceIDs.isEmpty) throw 'No connected iOS devices found.'; - return iosDeviceIds; + return iosDeviceIDs; } @override From 409414265eaa7954cfd6658d56f663ffe66c29ea Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Mon, 19 Jun 2017 21:10:58 -0700 Subject: [PATCH 36/51] Sample code for dialogs. (#10812) --- dev/bots/analyze-sample-code.dart | 1 + packages/flutter/lib/src/material/dialog.dart | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/dev/bots/analyze-sample-code.dart b/dev/bots/analyze-sample-code.dart index 7aff93c54a730..3b76826c277d7 100644 --- a/dev/bots/analyze-sample-code.dart +++ b/dev/bots/analyze-sample-code.dart @@ -130,6 +130,7 @@ Future main() async { } final List buffer = []; buffer.add('// generated code'); + buffer.add('import \'dart:async\';'); buffer.add('import \'dart:math\' as math;'); buffer.add('import \'dart:ui\' as ui;'); for (FileSystemEntity file in flutterPackage.listSync(recursive: false, followLinks: false)) { diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart index c4bc60fc0116e..9ff47ce37fa32 100644 --- a/packages/flutter/lib/src/material/dialog.dart +++ b/packages/flutter/lib/src/material/dialog.dart @@ -14,6 +14,9 @@ import 'ink_well.dart'; import 'material.dart'; import 'theme.dart'; +// Examples can assume: +// enum Department { treasury, state } + /// A material design dialog. /// /// This dialog widget does not have any opinion about the contents of the @@ -80,6 +83,39 @@ class Dialog extends StatelessWidget { /// Typically passed as the child widget to [showDialog], which displays the /// dialog. /// +/// ## Sample code +/// +/// This snippet shows a method in a [State] which, when called, displays a dialog box +/// and returns a [Future] that completes when the dialog is dismissed. +/// +/// ```dart +/// Future _neverSatisfied() async { +/// return showDialog( +/// context: context, +/// barrierDismissible: false, // user must tap button! +/// child: new AlertDialog( +/// title: new Text('Rewind and remember'), +/// content: new SingleChildScrollView( +/// child: new ListBody( +/// children: [ +/// new Text('You will never be satisfied.'), +/// new Text('You\’re like me. I’m never satisfied.'), +/// ], +/// ), +/// ), +/// actions: [ +/// new FlatButton( +/// child: new Text('Regret'), +/// onPressed: () { +/// Navigator.of(context).pop(); +/// }, +/// ), +/// ], +/// ), +/// ); +/// } +/// ``` +/// /// See also: /// /// * [SimpleDialog], which handles the scrolling of the contents but has no [actions]. @@ -185,6 +221,15 @@ class AlertDialog extends StatelessWidget { /// selects this option, the widget will call the [onPressed] callback, which /// typically uses [Navigator.pop] to close the dialog. /// +/// ## Sample code +/// +/// ```dart +/// new SimpleDialogOption( +/// onPressed: () { Navigator.pop(context, Department.treasury); }, +/// child: const Text('Treasury department'), +/// ) +/// ``` +/// /// See also: /// /// * [SimpleDialog], for a dialog in which to use this widget. @@ -203,6 +248,9 @@ class SimpleDialogOption extends StatelessWidget { /// The callback that is called when this option is selected. /// /// If this is set to null, the option cannot be selected. + /// + /// When used in a [SimpleDialog], this will typically call [Navigator.pop] + /// with a value for [showDialog] to complete its future with. final VoidCallback onPressed; /// The widget below this widget in the tree. @@ -233,6 +281,48 @@ class SimpleDialogOption extends StatelessWidget { /// Typically passed as the child widget to [showDialog], which displays the /// dialog. /// +/// ## Sample code +/// +/// In this example, the user is asked to select between two options. These +/// options are represented as an enum. The [showDialog] method here returns +/// a [Future] that completes to a value of that enum. If the user cancels +/// the dialog (e.g. by hitting the back button on Android, or tapping on the +/// mask behind the dialog) then the future completes with the null value. +/// +/// The return value in this example is used as the index for a switch statement. +/// One advantage of using an enum as the return value and then using that to +/// drive a switch statement is that the analyzer will flag any switch statement +/// that doesn't mention every value in the enum. +/// +/// ```dart +/// Future _askedToLead() async { +/// switch (await showDialog( +/// context: context, +/// child: new SimpleDialog( +/// title: const Text('Select assignment'), +/// children: [ +/// new SimpleDialogOption( +/// onPressed: () { Navigator.pop(context, Department.treasury); }, +/// child: const Text('Treasury department'), +/// ), +/// new SimpleDialogOption( +/// onPressed: () { Navigator.pop(context, Department.state); }, +/// child: const Text('State department'), +/// ), +/// ], +/// ), +/// )) { +/// case Department.treasury: +/// // Let's go. +/// // ... +/// break; +/// case Department.state: +/// // ... +/// break; +/// } +/// } +/// ``` +/// /// See also: /// /// * [SimpleDialogOption], which are options used in this type of dialog. From c14470e0c7c9c6a7ec5adf7651ef688a304cff09 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Tue, 20 Jun 2017 06:46:43 -0700 Subject: [PATCH 37/51] dont' validate against webstorm (#10828) --- packages/flutter_tools/lib/src/doctor.dart | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index e9d47cb900818..191c390b4b818 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -251,11 +251,9 @@ abstract class IntelliJValidator extends DoctorValidator { static final Map _idToTitle = { 'IntelliJIdea' : 'IntelliJ IDEA Ultimate Edition', 'IdeaIC' : 'IntelliJ IDEA Community Edition', - 'WebStorm': 'WebStorm', }; static final Version kMinIdeaVersion = new Version(2017, 1, 0); - static final Version kMinWebStormVersion = new Version(2017, 1, 0); static final Version kMinFlutterPluginVersion = new Version(14, 0, 0); static Iterable get installedValidators { @@ -272,11 +270,7 @@ abstract class IntelliJValidator extends DoctorValidator { _validatePackage(messages, 'flutter-intellij.jar', 'Flutter', minVersion: kMinFlutterPluginVersion); - - // Dart is bundled with WebStorm. - if (!isWebStorm) { - _validatePackage(messages, 'Dart', 'Dart'); - } + _validatePackage(messages, 'Dart', 'Dart'); if (_hasIssues(messages)) { messages.add(new ValidationMessage( @@ -285,7 +279,7 @@ abstract class IntelliJValidator extends DoctorValidator { )); } - _validateIntelliJVersion(messages, isWebStorm ? kMinWebStormVersion : kMinIdeaVersion); + _validateIntelliJVersion(messages, kMinIdeaVersion); return new ValidationResult( _hasIssues(messages) ? ValidationType.partial : ValidationType.installed, @@ -298,8 +292,6 @@ abstract class IntelliJValidator extends DoctorValidator { return messages.any((ValidationMessage message) => message.isError); } - bool get isWebStorm => title == 'WebStorm'; - void _validateIntelliJVersion(List messages, Version minVersion) { // Ignore unknown versions. if (minVersion == Version.unknown) @@ -429,7 +421,6 @@ class IntelliJValidatorOnMac extends IntelliJValidator { 'IntelliJ IDEA.app' : 'IntelliJIdea', 'IntelliJ IDEA Ultimate.app' : 'IntelliJIdea', 'IntelliJ IDEA CE.app' : 'IdeaIC', - 'WebStorm.app': 'WebStorm', }; static Iterable get installed { From b471a9cffcd1550ae562ffa2491b578fbb24bf08 Mon Sep 17 00:00:00 2001 From: Seth Ladd Date: Tue, 20 Jun 2017 08:20:57 -0700 Subject: [PATCH 38/51] send channel name as a custom dimension (#10814) * send channel name as a custom dimension * tweaks from review * remove unused import --- packages/flutter_tools/lib/src/usage.dart | 3 ++- packages/flutter_tools/lib/src/version.dart | 12 +++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/lib/src/usage.dart b/packages/flutter_tools/lib/src/usage.dart index 76e8e8ce7521c..3f122a0e5b7d2 100644 --- a/packages/flutter_tools/lib/src/usage.dart +++ b/packages/flutter_tools/lib/src/usage.dart @@ -26,8 +26,9 @@ class Usage { _analytics = new AnalyticsIO(_kFlutterUA, settingsName, version); // Report a more detailed OS version string than package:usage does by - // default as custom dimension 1 (configured in our analytics account). + // default. Also, send the branch name as the "channel". _analytics.setSessionValue('dimension1', os.name); + _analytics.setSessionValue('dimension2', FlutterVersion.getBranchName(whitelistBranchName: true)); bool runningOnCI = false; diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart index b41a77c070519..9cc6a77846b19 100644 --- a/packages/flutter_tools/lib/src/version.dart +++ b/packages/flutter_tools/lib/src/version.dart @@ -149,6 +149,16 @@ class FlutterVersion { String commit = _shortGitRevision(_runSync(['git', 'rev-parse', 'HEAD'])); commit = commit.isEmpty ? 'unknown' : commit; + final String branch = getBranchName(whitelistBranchName: whitelistBranchName); + + return '$branch/$commit'; + } + + /// Return the branch name. + /// + /// If whitelistBranchName is true and the branch is unknown, + /// the branch name will be returned as 'dev'. + static String getBranchName({ bool whitelistBranchName: false }) { String branch = _runSync(['git', 'rev-parse', '--abbrev-ref', 'HEAD']); branch = branch == 'HEAD' ? 'master' : branch; @@ -158,7 +168,7 @@ class FlutterVersion { branch = 'dev'; } - return '$branch/$commit'; + return branch; } /// The amount of time we wait before pinging the server to check for the From 40db1e4bc6a04d577fded555c135096417f8201d Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Tue, 20 Jun 2017 09:33:26 -0700 Subject: [PATCH 39/51] Added InputDecoration helperText, helperStyle (#10852) --- .../demo/material/text_form_field_demo.dart | 1 + .../lib/src/material/input_decorator.dart | 40 +++++++++-- .../test/material/text_field_test.dart | 69 +++++++++++++++++++ 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart b/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart index 7abfade4cafdb..54ce4fba3597c 100644 --- a/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart @@ -144,6 +144,7 @@ class TextFormFieldDemoState extends State { new TextFormField( decoration: const InputDecoration( hintText: 'Tell us about yourself', + helperText: 'Keep it short, this is just a demo', labelText: 'Life story', ), maxLines: 3, diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index 668d897a54810..fe422e4865bea 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -31,6 +31,8 @@ class InputDecoration { this.icon, this.labelText, this.labelStyle, + this.helperText, + this.helperStyle, this.hintText, this.hintStyle, this.errorText, @@ -55,6 +57,8 @@ class InputDecoration { }) : icon = null, labelText = null, labelStyle = null, + helperText = null, + helperStyle = null, errorText = null, errorStyle = null, isDense = false, @@ -93,6 +97,17 @@ class InputDecoration { /// input field and the current [Theme]. final TextStyle labelStyle; + /// Text that provides context about the field’s value, such as how the value + /// will be used. + /// + /// If non-null, the text is displayed below the input field, in the same + /// location as [errorText]. If a non-null [errorText] value is specified then + /// the helper text is not shown. + final String helperText; + + /// The style to use for the [helperText]. + final TextStyle helperStyle; + /// Text that suggests what sort of input the field accepts. /// /// Displayed on top of the input field (i.e., at the same location on the @@ -113,7 +128,7 @@ class InputDecoration { /// Text that appears below the input field. /// - /// If non-null the divider, that appears below the input field is red. + /// If non-null, the divider that appears below the input field is red. final String errorText; /// The style to use for the [errorText]. @@ -171,6 +186,8 @@ class InputDecoration { Widget icon, String labelText, TextStyle labelStyle, + String helperText, + TextStyle helperStyle, String hintText, TextStyle hintStyle, String errorText, @@ -186,6 +203,8 @@ class InputDecoration { icon: icon ?? this.icon, labelText: labelText ?? this.labelText, labelStyle: labelStyle ?? this.labelStyle, + helperText: helperText ?? this.helperText, + helperStyle: helperStyle ?? this.helperStyle, hintText: hintText ?? this.hintText, hintStyle: hintStyle ?? this.hintStyle, errorText: errorText ?? this.errorText, @@ -209,6 +228,8 @@ class InputDecoration { return typedOther.icon == icon && typedOther.labelText == labelText && typedOther.labelStyle == labelStyle + && typedOther.helperText == helperText + && typedOther.helperStyle == helperStyle && typedOther.hintText == hintText && typedOther.hintStyle == hintStyle && typedOther.errorText == errorText @@ -228,6 +249,8 @@ class InputDecoration { icon, labelText, labelStyle, + helperText, + helperStyle, hintText, hintStyle, errorText, @@ -249,6 +272,8 @@ class InputDecoration { description.add('icon: $icon'); if (labelText != null) description.add('labelText: "$labelText"'); + if (helperText != null) + description.add('helperText: "$helperText"'); if (hintText != null) description.add('hintText: "$hintText"'); if (errorText != null) @@ -390,6 +415,7 @@ class InputDecorator extends StatelessWidget { assert(!isDense || !isCollapsed); final String labelText = decoration.labelText; + final String helperText = decoration.helperText; final String hintText = decoration.hintText; final String errorText = decoration.errorText; @@ -485,16 +511,20 @@ class InputDecorator extends StatelessWidget { stackChildren.add(_buildContent(borderColor, topPadding, isDense, inputChild)); } - if (!isDense && errorText != null) { + if (!isDense && (errorText != null || helperText != null)) { assert(!isCollapsed); - final TextStyle errorStyle = decoration.errorStyle ?? themeData.textTheme.caption.copyWith(color: themeData.errorColor); + final TextStyle captionStyle = themeData.textTheme.caption; + final TextStyle subtextStyle = errorText != null + ? decoration.errorStyle ?? captionStyle.copyWith(color: themeData.errorColor) + : decoration.helperStyle ?? captionStyle.copyWith(color: themeData.hintColor); + stackChildren.add(new Positioned( left: 0.0, right: 0.0, bottom: 0.0, child: new Text( - errorText, - style: errorStyle, + errorText ?? helperText, + style: subtextStyle, textAlign: textAlign, overflow: TextOverflow.ellipsis, ), diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index f9ea072cc7e87..7382e83bf40bf 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -741,6 +741,75 @@ void main() { await checkText('Hello World'); }); + testWidgets('TextField errorText trumps helperText', (WidgetTester tester) async { + Widget builder() { + return const Center( + child: const Material( + child: const TextField( + decoration: const InputDecoration( + errorText: 'error text', + helperText: 'helper text', + ), + ), + ), + ); + } + + await tester.pumpWidget(builder()); + expect(find.text('helper text'), findsNothing); + expect(find.text('error text'), findsOneWidget); + }); + + testWidgets('TextField with default helperStyle', (WidgetTester tester) async { + final ThemeData themeData = new ThemeData( + hintColor: Colors.blue[500], + ); + + Widget builder() { + return new Center( + child: new Theme( + data: themeData, + child: const Material( + child: const TextField( + decoration: const InputDecoration( + helperText: 'helper text', + ), + ), + ), + ), + ); + } + + await tester.pumpWidget(builder()); + final Text helperText = tester.widget(find.text('helper text')); + expect(helperText.style.color, themeData.hintColor); + expect(helperText.style.fontSize, themeData.textTheme.caption.fontSize); + }); + + testWidgets('TextField with specified helperStyle', (WidgetTester tester) async { + final TextStyle style = new TextStyle( + color: Colors.pink[500], + fontSize: 10.0, + ); + + Widget builder() { + return new Center( + child: new Material( + child: new TextField( + decoration: new InputDecoration( + helperText: 'helper text', + helperStyle: style, + ), + ), + ), + ); + } + + await tester.pumpWidget(builder()); + final Text helperText = tester.widget(find.text('helper text')); + expect(helperText.style, style); + }); + testWidgets('TextField with default hintStyle', (WidgetTester tester) async { final TextStyle style = new TextStyle( color: Colors.pink[500], From 6d32b339972bcc0422b7a0458447d9d44ce0bab5 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Tue, 20 Jun 2017 09:35:15 -0700 Subject: [PATCH 40/51] Text selection handles track scrolled text fields (#10805) Introduce CompositedTransformTarget and CompositedTransformFollower widgets, corresponding render objects, and corresponding layers. Adjust the way text fields work to use this. Various changes I needed to debug the issues that came up. --- .../foundation/tree_diagnostics_mixin.dart | 53 ++- .../lib/src/material/text_selection.dart | 41 +- packages/flutter/lib/src/rendering/box.dart | 2 +- packages/flutter/lib/src/rendering/debug.dart | 7 +- .../flutter/lib/src/rendering/editable.dart | 16 +- packages/flutter/lib/src/rendering/layer.dart | 406 +++++++++++++++++- .../flutter/lib/src/rendering/object.dart | 72 +++- .../flutter/lib/src/rendering/proxy_box.dart | 198 ++++++++- packages/flutter/lib/src/widgets/basic.dart | 283 +++++++++--- .../lib/src/widgets/editable_text.dart | 32 +- .../lib/src/widgets/scroll_controller.dart | 4 + .../lib/src/widgets/text_selection.dart | 83 ++-- .../test/material/text_field_test.dart | 65 ++- .../widgets/composited_transform_test.dart | 166 +++++++ 14 files changed, 1249 insertions(+), 179 deletions(-) create mode 100644 packages/flutter/test/widgets/composited_transform_test.dart diff --git a/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart b/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart index 3c634144d308e..cb69a74f6463e 100644 --- a/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart +++ b/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart @@ -4,23 +4,71 @@ import 'package:meta/meta.dart'; +import 'print.dart'; + /// A mixin that helps dump string representations of trees. abstract class TreeDiagnosticsMixin { // This class is intended to be used as a mixin, and should not be // extended directly. factory TreeDiagnosticsMixin._() => null; + /// A brief description of this object, usually just the [runtimeType] and the + /// [hashCode]. + /// + /// See also: + /// + /// * [toStringShallow], for a detailed description of the object. + /// * [toStringDeep], for a description of the subtree rooted at this object. @override String toString() => '$runtimeType#$hashCode'; + /// Returns a one-line detailed description of the object. + /// + /// This description includes everything from [debugFillDescription], but does + /// not recurse to any children. + /// + /// The [toStringShallow] method can take an argument, which is the string to + /// place between each part obtained from [debugFillDescription]. Passing a + /// string such as `'\n '` will result in a multiline string that indents the + /// properties of the object below its name (as per [toString]). + /// + /// See also: + /// + /// * [toString], for a brief description of the object. + /// * [toStringDeep], for a description of the subtree rooted at this object. + String toStringShallow([String joiner = '; ']) { + final StringBuffer result = new StringBuffer(); + result.write(toString()); + result.write(joiner); + final List description = []; + debugFillDescription(description); + result.write(description.join(joiner)); + return result.toString(); + } + /// Returns a string representation of this node and its descendants. + /// + /// This includes the information from [debugFillDescription], and then + /// recurses into the children using [debugDescribeChildren]. + /// + /// The [toStringDeep] method takes arguments, but those are intended for + /// internal use when recursing to the descendants, and so can be ignored. + /// + /// See also: + /// + /// * [toString], for a brief description of the object but not its children. + /// * [toStringShallow], for a detailed description of the object but not its + /// children. String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) { String result = '$prefixLineOne$this\n'; final String childrenDescription = debugDescribeChildren(prefixOtherLines); final String descriptionPrefix = childrenDescription != '' ? '$prefixOtherLines \u2502 ' : '$prefixOtherLines '; final List description = []; debugFillDescription(description); - result += description.map((String description) => '$descriptionPrefix$description\n').join(); + result += description + .expand((String description) => debugWordWrap(description, 65, wrapIndent: ' ')) + .map((String line) => "$descriptionPrefix$line\n") + .join(); if (childrenDescription == '') { final String prefix = prefixOtherLines.trimRight(); if (prefix != '') @@ -31,7 +79,8 @@ abstract class TreeDiagnosticsMixin { return result; } - /// Add additional information to the given description for use by [toStringDeep]. + /// Add additional information to the given description for use by + /// [toStringDeep] and [toStringShallow]. @protected @mustCallSuper void debugFillDescription(List description) { } diff --git a/packages/flutter/lib/src/material/text_selection.dart b/packages/flutter/lib/src/material/text_selection.dart index f696c41db4c41..719f466e15875 100644 --- a/packages/flutter/lib/src/material/text_selection.dart +++ b/packages/flutter/lib/src/material/text_selection.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:math' as math; import 'package:flutter/widgets.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'flat_button.dart'; @@ -90,8 +91,17 @@ class _TextSelectionToolbar extends StatelessWidget { /// Centers the toolbar around the given position, ensuring that it remains on /// screen. class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate { - _TextSelectionToolbarLayout(this.position); + _TextSelectionToolbarLayout(this.screenSize, this.globalEditableRegion, this.position); + /// The size of the screen at the time that the toolbar was last laid out. + final Size screenSize; + + /// Size and position of the editing region at the time the toolbar was last + /// laid out, in global coordinates. + final Rect globalEditableRegion; + + /// Anchor position of the toolbar, relative to the top left of the + /// [globalEditableRegion]. final Offset position; @override @@ -101,17 +111,20 @@ class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate { @override Offset getPositionForChild(Size size, Size childSize) { - double x = position.dx - childSize.width / 2.0; - double y = position.dy - childSize.height; + final Offset globalPosition = globalEditableRegion.topLeft + position; + + double x = globalPosition.dx - childSize.width / 2.0; + double y = globalPosition.dy - childSize.height; if (x < _kToolbarScreenPadding) x = _kToolbarScreenPadding; - else if (x + childSize.width > size.width - 2 * _kToolbarScreenPadding) - x = size.width - childSize.width - _kToolbarScreenPadding; + else if (x + childSize.width > screenSize.width - _kToolbarScreenPadding) + x = screenSize.width - childSize.width - _kToolbarScreenPadding; + if (y < _kToolbarScreenPadding) y = _kToolbarScreenPadding; - else if (y + childSize.height > size.height - 2 * _kToolbarScreenPadding) - y = size.height - childSize.height - _kToolbarScreenPadding; + else if (y + childSize.height > screenSize.height - _kToolbarScreenPadding) + y = screenSize.height - childSize.height - _kToolbarScreenPadding; return new Offset(x, y); } @@ -149,15 +162,17 @@ class _MaterialTextSelectionControls extends TextSelectionControls { /// Builder for material-style copy/paste text selection toolbar. @override - Widget buildToolbar( - BuildContext context, Offset position, TextSelectionDelegate delegate) { + Widget buildToolbar(BuildContext context, Rect globalEditableRegion, Offset position, TextSelectionDelegate delegate) { assert(debugCheckHasMediaQuery(context)); - final Size screenSize = MediaQuery.of(context).size; return new ConstrainedBox( - constraints: new BoxConstraints.loose(screenSize), + constraints: new BoxConstraints.tight(globalEditableRegion.size), child: new CustomSingleChildLayout( - delegate: new _TextSelectionToolbarLayout(position), - child: new _TextSelectionToolbar(delegate) + delegate: new _TextSelectionToolbarLayout( + MediaQuery.of(context).size, + globalEditableRegion, + position, + ), + child: new _TextSelectionToolbar(delegate), ) ); } diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index 065e5da6f20c2..270820fd50812 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -1829,7 +1829,7 @@ abstract class RenderBox extends RenderObject { /// Subclasses that apply transforms during painting should override this /// function to factor those transforms into the calculation. /// - /// The RenderBox implementation takes care of adjusting the matrix for the + /// The [RenderBox] implementation takes care of adjusting the matrix for the /// position of the given child as determined during layout and stored on the /// child's [parentData] in the [BoxParentData.offset] field. @override diff --git a/packages/flutter/lib/src/rendering/debug.dart b/packages/flutter/lib/src/rendering/debug.dart index 54610e13ba5e7..6183b4bec16f4 100644 --- a/packages/flutter/lib/src/rendering/debug.dart +++ b/packages/flutter/lib/src/rendering/debug.dart @@ -123,8 +123,13 @@ bool debugCheckIntrinsicSizes = false; bool debugProfilePaintsEnabled = false; -/// Returns a list of strings representing the given transform in a format useful for [RenderObject.debugFillDescription]. +/// Returns a list of strings representing the given transform in a format +/// useful for [RenderObject.debugFillDescription]. +/// +/// If the argument is null, returns a list with the single string "null". List debugDescribeTransform(Matrix4 transform) { + if (transform == null) + return const ['null']; final List matrix = transform.toString().split('\n').map((String s) => ' $s').toList(); matrix.removeLast(); return matrix; diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index aa5c516b0ba6f..b28f39ddfa21a 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -30,8 +30,9 @@ typedef void SelectionChangedHandler(TextSelection selection, RenderEditable ren /// Used by [RenderEditable.onCaretChanged]. typedef void CaretChangedHandler(Rect caretRect); -/// Represents a global screen coordinate of the point in a selection, and the -/// text direction at that point. +/// Represents the coordinates of the point in a selection, and the text +/// direction at that point, relative to top left of the [RenderEditable] that +/// holds the selection. @immutable class TextSelectionPoint { /// Creates a description of a point in a text selection. @@ -40,7 +41,8 @@ class TextSelectionPoint { const TextSelectionPoint(this.point, this.direction) : assert(point != null); - /// Screen coordinates of the lower left or lower right corner of the selection. + /// Coordinates of the lower left or lower right corner of the selection, + /// relative to the top left of the [RenderEditable] object. final Offset point; /// Direction of the text at this edge of the selection. @@ -316,7 +318,7 @@ class RenderEditable extends RenderBox { bool _hasVisualOverflow = false; - /// Returns the global coordinates of the endpoints of the given selection. + /// Returns the local coordinates of the endpoints of the given selection. /// /// If the selection is collapsed (and therefore occupies a single point), the /// returned list is of length one. Otherwise, the selection is not collapsed @@ -333,14 +335,14 @@ class RenderEditable extends RenderBox { // TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary. final Offset caretOffset = _textPainter.getOffsetForCaret(selection.extent, _caretPrototype); final Offset start = new Offset(0.0, _preferredLineHeight) + caretOffset + paintOffset; - return [new TextSelectionPoint(localToGlobal(start), null)]; + return [new TextSelectionPoint(start, null)]; } else { final List boxes = _textPainter.getBoxesForSelection(selection); final Offset start = new Offset(boxes.first.start, boxes.first.bottom) + paintOffset; final Offset end = new Offset(boxes.last.end, boxes.last.bottom) + paintOffset; return [ - new TextSelectionPoint(localToGlobal(start), boxes.first.direction), - new TextSelectionPoint(localToGlobal(end), boxes.last.direction), + new TextSelectionPoint(start, boxes.first.direction), + new TextSelectionPoint(end, boxes.last.direction), ]; } } diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart index da25813b60800..453fe509e1640 100644 --- a/packages/flutter/lib/src/rendering/layer.dart +++ b/packages/flutter/lib/src/rendering/layer.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:collection'; import 'dart:ui' as ui show ImageFilter, Picture, SceneBuilder; import 'dart:ui' show Offset; @@ -90,7 +91,7 @@ abstract class Layer extends AbstractNode with TreeDiagnosticsMixin { /// Override this method to upload this layer to the engine. /// - /// The layerOffset is the accumulated offset of this layer's parent from the + /// The `layerOffset` is the accumulated offset of this layer's parent from the /// origin of the builder's coordinate system. void addToScene(ui.SceneBuilder builder, Offset layerOffset); @@ -117,6 +118,16 @@ abstract class Layer extends AbstractNode with TreeDiagnosticsMixin { /// /// Picture layers are always leaves in the layer tree. class PictureLayer extends Layer { + PictureLayer(this.canvasBounds); + + /// The bounds that were used for the canvas that drew this layer's [picture]. + /// + /// This is purely advisory. It is included in the information dumped with + /// [dumpLayerTree] (which can be triggered by pressing "L" when using + /// "flutter run" at the console), which can help debug why certain drawing + /// commands are being culled. + final Rect canvasBounds; + /// The picture recorded for this layer. /// /// The picture's coodinate system matches this layer's coodinate system. @@ -150,6 +161,12 @@ class PictureLayer extends Layer { void addToScene(ui.SceneBuilder builder, Offset layerOffset) { builder.addPicture(layerOffset, picture, isComplexHint: isComplexHint, willChangeHint: willChangeHint); } + + @override + void debugFillDescription(List description) { + super.debugFillDescription(description); + description.add('paint bounds: $canvasBounds'); + } } /// A layer that indicates to the compositor that it should display @@ -357,6 +374,44 @@ class ContainerLayer extends Layer { } } + /// Applies the transform that would be applied when compositing the given + /// child to the given matrix. + /// + /// Specifically, this should apply the transform that is applied to child's + /// _origin_. When using [applyTransform] with a chain of layers, results will + /// be unreliable unless the deepest layer in the chain collapses the + /// `layerOffset` in [addToScene] to zero, meaning that it passes + /// [Offset.zero] to its children, and bakes any incoming `layerOffset` into + /// the [SceneBuilder] as (for instance) a transform (which is then also + /// included in the transformation applied by [applyTransform]). + /// + /// For example, if [addToScene] applies the `layerOffset` and then + /// passes [Offset.zero] to the children, then it should be included in the + /// transform applied here, whereas if [addToScene] just passes the + /// `layerOffset` to the child, then it should not be included in the + /// transform applied here. + /// + /// This method is only valid immediately after [addToScene] has been called, + /// before any of the properties have been changed. + /// + /// The default implementation does nothing, since [ContainerLayer], by + /// default, composits its children at the origin of the [ContainerLayer] + /// itself. + /// + /// The `child` argument should generally not be null, since in principle a + /// layer could transform each child independently. However, certain layers + /// may explicitly allow null as a value, for example if they know that they + /// transform all their children identically. + /// + /// The `transform` argument must not be null. + /// + /// Used by [FollowerLayer] to transform its child to a [LeaderLayer]'s + /// position. + void applyTransform(Layer child, Matrix4 transform) { + assert(child != null); + assert(transform != null); + } + @override String debugDescribeChildren(String prefix) { if (firstChild == null) @@ -391,13 +446,17 @@ class ContainerLayer extends Layer { class OffsetLayer extends ContainerLayer { /// Creates an offset layer. /// - /// By default, [offset] is zero. + /// By default, [offset] is zero. It must be non-null before the compositing + /// phase of the pipeline. OffsetLayer({ this.offset: Offset.zero }); /// Offset from parent in the parent's coordinate system. /// /// The scene must be explicitly recomposited after this property is changed /// (as described at [Layer]). + /// + /// The [offset] property must be non-null before the compositing phase of the + /// pipeline. Offset offset; @override @@ -412,7 +471,6 @@ class OffsetLayer extends ContainerLayer { } } - /// A composite layer that clips its children using a rectangle. class ClipRectLayer extends ContainerLayer { /// Creates a layer with a rectangular clip. @@ -497,35 +555,51 @@ class ClipPathLayer extends ContainerLayer { } } -/// A composited layer that applies a transformation matrix to its children. +/// A composited layer that applies a given transformation matrix to its +/// children. +/// +/// This class inherits from [OffsetLayer] to make it one of the layers that +/// can be used at the root of a [RenderObject] hierarchy. class TransformLayer extends OffsetLayer { /// Creates a transform layer. /// - /// The [transform] property must be non-null before the compositing phase of - /// the pipeline. - TransformLayer({ - this.transform - }); + /// The [transform] and [offset] properties must be non-null before the + /// compositing phase of the pipeline. + TransformLayer({ this.transform, Offset offset: Offset.zero }) : super(offset: offset); /// The matrix to apply. /// /// The scene must be explicitly recomposited after this property is changed /// (as described at [Layer]). + /// + /// This transform is applied before [offset], if both are set. + /// + /// The [transform] property must be non-null before the compositing phase of + /// the pipeline. Matrix4 transform; + Matrix4 _lastEffectiveTransform; + @override void addToScene(ui.SceneBuilder builder, Offset layerOffset) { - assert(offset == Offset.zero); - Matrix4 effectiveTransform = transform; - if (layerOffset != Offset.zero) { - effectiveTransform = new Matrix4.translationValues(layerOffset.dx, layerOffset.dy, 0.0) - ..multiply(transform); + _lastEffectiveTransform = transform; + final Offset totalOffset = offset + layerOffset; + if (totalOffset != Offset.zero) { + _lastEffectiveTransform = new Matrix4.translationValues(totalOffset.dx, totalOffset.dy, 0.0) + ..multiply(_lastEffectiveTransform); } - builder.pushTransform(effectiveTransform.storage); + builder.pushTransform(_lastEffectiveTransform.storage); addChildrenToScene(builder, Offset.zero); builder.pop(); } + @override + void applyTransform(Layer child, Matrix4 transform) { + assert(child != null); + assert(transform != null); + transform.multiply(_lastEffectiveTransform); + } + @override void debugFillDescription(List description) { super.debugFillDescription(description); @@ -565,7 +639,7 @@ class OpacityLayer extends ContainerLayer { } } -/// A composited layer that applies a shader to hits children. +/// A composited layer that applies a shader to its children. class ShaderMaskLayer extends ContainerLayer { /// Creates a shader mask layer. /// @@ -682,3 +756,303 @@ class PhysicalModelLayer extends ContainerLayer { description.add('clipRRect: $clipRRect'); } } + +/// An object that a [LeaderLayer] can register with. +/// +/// An instance of this class should be provided as the [LeaderLayer.link] and +/// the [FollowerLayer.link] properties to cause the [FollowerLayer] to follow +/// the [LeaderLayer]. +/// +/// See also: +/// +/// * [CompositedTransformTarget], the widget that creates a [LeaderLayer]. +/// * [CompositedTransformFollower], the widget that creates a [FollowerLayer]. +/// * [RenderLeaderLayer] and [RenderFollowerLayer], the corresponding +/// render objects. +class LayerLink { + /// The currently-registered [LeaderLayer], if any. + LeaderLayer get leader => _leader; + LeaderLayer _leader; + + @override + String toString() => '$runtimeType#$hashCode(${ _leader != null ? "" : "" })'; +} + +/// A composited layer that can be followed by a [FollowerLayer]. +/// +/// This layer collapses the accumulated offset into a transform and passes +/// [Offset.zero] to its child layers in the [addToScene]/[addChildrenToScene] +/// methods, so that [applyTransform] will work reliably. +class LeaderLayer extends ContainerLayer { + /// Creates a leader layer. + /// + /// The [link] property must not be null, and must not have been provided to + /// any other [LeaderLayer] layers that are [attached] to the layer tree at + /// the same time. + /// + /// The [offset] property must be non-null before the compositing phase of the + /// pipeline. + LeaderLayer({ @required this.link, this.offset: Offset.zero }) : assert(link != null); + + /// The object with which this layer should register. + /// + /// The link will be established when this layer is [attach]ed, and will be + /// cleared when this layer is [detach]ed. + final LayerLink link; + + /// Offset from parent in the parent's coordinate system. + /// + /// The scene must be explicitly recomposited after this property is changed + /// (as described at [Layer]). + /// + /// The [offset] property must be non-null before the compositing phase of the + /// pipeline. + Offset offset; + + @override + void attach(Object owner) { + super.attach(owner); + assert(link.leader == null); + _lastOffset = null; + link._leader = this; + } + + @override + void detach() { + assert(link.leader == this); + link._leader = null; + _lastOffset = null; + super.detach(); + } + + /// The offset the last time this layer was composited. + /// + /// This is reset to null when the layer is attached or detached, to help + /// catch cases where the follower layer ends up before the leader layer, but + /// not every case can be detected. + Offset _lastOffset; + + @override + void addToScene(ui.SceneBuilder builder, Offset layerOffset) { + assert(offset != null); + _lastOffset = offset + layerOffset; + if (_lastOffset != Offset.zero) + builder.pushTransform(new Matrix4.translationValues(_lastOffset.dx, _lastOffset.dy, 0.0).storage); + addChildrenToScene(builder, Offset.zero); + if (_lastOffset != Offset.zero) + builder.pop(); + } + + /// Applies the transform that would be applied when compositing the given + /// child to the given matrix. + /// + /// See [ContainerLayer.applyTransform] for details. + /// + /// The `child` argument may be null, as the same transform is applied to all + /// children. + @override + void applyTransform(Layer child, Matrix4 transform) { + assert(_lastOffset != null); + if (_lastOffset != Offset.zero) + transform.translate(_lastOffset.dx, _lastOffset.dy); + } + + @override + void debugFillDescription(List description) { + super.debugFillDescription(description); + description.add('offset: $offset'); + description.add('link: $link'); + } +} + +/// A composited layer that applies a transformation matrix to its children such +/// that they are positioned to match a [LeaderLayer]. +/// +/// If any of the ancestors of this layer have a degenerate matrix (e.g. scaling +/// by zero), then the [FollowerLayer] will not be able to transform its child +/// to the coordinate space of the [Leader]. +/// +/// A [linkedOffset] property can be provided to further offset the child layer +/// from the leader layer, for example if the child is to follow the linked +/// layer at a distance rather than directly overlapping it. +class FollowerLayer extends ContainerLayer { + /// Creates a follower layer. + /// + /// The [link] property must not be null. + /// + /// The [unlinkedOffset], [linkedOffset], and [showWhenUnlinked] properties + /// must be non-null before the compositing phase of the pipeline. + FollowerLayer({ + @required this.link, + this.showWhenUnlinked: true, + this.unlinkedOffset: Offset.zero, + this.linkedOffset: Offset.zero, + }) : assert(link != null); + + /// The link to the [LeaderLayer]. + /// + /// The same object should be provided to a [LeaderLayer] that is earlier in + /// the layer tree. When this layer is composited, it will apply a transform + /// that moves its children to match the position of the [LeaderLayer]. + final LayerLink link; + + /// Whether to show the layer's contents when the [link] does not point to a + /// [LeaderLayer]. + /// + /// When the layer is linked, children layers are positioned such that they + /// have the same global position as the linked [LeaderLayer]. + /// + /// When the layer is not linked, then: if [showWhenUnlinked] is true, + /// children are positioned as if the [FollowerLayer] was a [ContainerLayer]; + /// if it is false, then children are hidden. + /// + /// The [showWhenUnlinked] property must be non-null before the compositing + /// phase of the pipeline. + bool showWhenUnlinked; + + /// Offset from parent in the parent's coordinate system, used when the layer + /// is not linked to a [LeaderLayer]. + /// + /// The scene must be explicitly recomposited after this property is changed + /// (as described at [Layer]). + /// + /// The [unlinkedOffset] property must be non-null before the compositing + /// phase of the pipeline. + /// + /// See also: + /// + /// * [linkedOffset], for when the layers are linked. + Offset unlinkedOffset; + + /// Offset from the origin of the leader layer to the origin of the child + /// layers, used when the layer is linked to a [LeaderLayer]. + /// + /// The scene must be explicitly recomposited after this property is changed + /// (as described at [Layer]). + /// + /// The [linkedOffset] property must be non-null before the compositing phase + /// of the pipeline. + /// + /// See also: + /// + /// * [unlinkedOffset], for when the layer is not linked. + Offset linkedOffset; + + Offset _lastOffset; + Matrix4 _lastTransform; + + /// The transform that was used during the last composition phase. + /// + /// If the [link] was not linked to a [LeaderLayer], or if this layer has + /// a degerenate matrix applied, then this will be null. + /// + /// This method returns a new [Matrix4] instance each time it is invoked. + Matrix4 getLastTransform() { + if (_lastTransform == null) + return null; + final Matrix4 result = new Matrix4.translationValues(-_lastOffset.dx, -_lastOffset.dy, 0.0); + result.multiply(_lastTransform); + return result; + } + + /// Call [applyTransform] for each layer in the provided list. + /// + /// The list is in reverse order (deepest first). The first layer will be + /// treated as the child of the second, and so forth. The first layer in the + /// list won't have [applyTransform] called on it. The first layer may be + /// null. + Matrix4 _collectTransformForLayerChain(List layers) { + // Initialize our result matrix. + final Matrix4 result = new Matrix4.identity(); + // Apply each layer to the matrix in turn, starting from the last layer, + // and providing the previous layer as the child. + for (int index = layers.length - 1; index > 0; index -= 1) + layers[index].applyTransform(layers[index - 1], result); + return result; + } + + /// Populate [_lastTransform] given the current state of the tree. + void _establishTransform() { + assert(link != null); + _lastTransform = null; + // Check to see if we are linked. + if (link.leader == null) + return; + // If we're linked, check the link is valid. + assert(link.leader.owner == owner, 'Linked LeaderLayer anchor is not in the same layer tree as the FollowerLayer.'); + assert(link.leader._lastOffset != null, 'LeaderLayer anchor must come before FollowerLayer in paint order, but the reverse was true.'); + // Collect all our ancestors into a Set so we can recognize them. + final Set ancestors = new HashSet(); + Layer ancestor = parent; + while (ancestor != null) { + ancestors.add(ancestor); + ancestor = ancestor.parent; + } + // Collect all the layers from a hypothetical child (null) of the target + // layer up to the common ancestor layer. + ContainerLayer layer = link.leader; + final List forwardLayers = [null, layer]; + do { + layer = layer.parent; + forwardLayers.add(layer); + } while (!ancestors.contains(layer)); + ancestor = layer; + // Collect all the layers from this layer up to the common ancestor layer. + layer = this; + final List inverseLayers = [layer]; + do { + layer = layer.parent; + inverseLayers.add(layer); + } while (layer != ancestor); + // Establish the forward and backward matrices given these lists of layers. + final Matrix4 forwardTransform = _collectTransformForLayerChain(forwardLayers); + final Matrix4 inverseTransform = _collectTransformForLayerChain(inverseLayers); + if (inverseTransform.invert() == 0.0) { + // We are in a degenerate transform, so there's not much we can do. + return; + } + // Combine the matrices and store the result. + inverseTransform.multiply(forwardTransform); + inverseTransform.translate(linkedOffset.dx, linkedOffset.dy); + _lastTransform = inverseTransform; + } + + @override + void addToScene(ui.SceneBuilder builder, Offset layerOffset) { + assert(link != null); + assert(showWhenUnlinked != null); + if (link.leader == null && !showWhenUnlinked) { + _lastTransform = null; + _lastOffset = null; + return; + } + _establishTransform(); + if (_lastTransform != null) { + builder.pushTransform(_lastTransform.storage); + addChildrenToScene(builder, Offset.zero); + builder.pop(); + _lastOffset = unlinkedOffset + layerOffset; + } else { + _lastOffset = null; + addChildrenToScene(builder, unlinkedOffset + layerOffset); + } + } + + @override + void applyTransform(Layer child, Matrix4 transform) { + assert(child != null); + assert(transform != null); + if (_lastTransform != null) + transform.multiply(_lastTransform); + } + + @override + void debugFillDescription(List description) { + super.debugFillDescription(description); + description.add('link: $link'); + if (_lastTransform != null) { + description.add('transform:'); + description.addAll(debugDescribeTransform(getLastTransform())); + } + } +} diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 1cad998d3f7c3..6e248e0600451 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -56,13 +56,29 @@ typedef void PaintingContextCallback(PaintingContext context, Offset offset); /// child might be recorded in separate compositing layers. For this reason, do /// not hold a reference to the canvas across operations that might paint /// child render objects. +/// +/// New [PaintingContext] objects are created automatically when using +/// [PaintingContext.repaintCompositedChild] and [pushLayer]. class PaintingContext { - PaintingContext._(this._containerLayer, this._paintBounds) + PaintingContext._(this._containerLayer, this.canvasBounds) : assert(_containerLayer != null), - assert(_paintBounds != null); + assert(canvasBounds != null); final ContainerLayer _containerLayer; - final Rect _paintBounds; + + /// The bounds within which the painting context's [canvas] will record + /// painting commands. + /// + /// A render object provided with this [PaintingContext] (e.g. in its + /// [RenderObject.paint] method) is permitted to paint outside the region that + /// the render object occupies during layout, but is not permitted to paint + /// outside these paints bounds. These paint bounds are used to construct + /// memory-efficient composited layers, which means attempting to paint + /// outside these bounds can attempt to write to pixels that do not exist in + /// the composited layer. + /// + /// The [paintBounds] rectangle is in the [canvas] coordinate system. + final Rect canvasBounds; /// Repaint the given render object. /// @@ -70,6 +86,11 @@ class PaintingContext { /// composited layer, and must be in need of painting. The render object's /// layer, if any, is re-used, along with any layers in the subtree that don't /// need to be repainted. + /// + /// See also: + /// + /// * [RenderObject.isRepaintBoundary], which determines if a [RenderObject] + /// has a composited layer. static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent: false }) { assert(child.isRepaintBoundary); assert(child._needsPaint); @@ -97,7 +118,7 @@ class PaintingContext { childContext._stopRecordingIfNeeded(); } - /// Paint a child render object. + /// Paint a child [RenderObject]. /// /// If the child has its own composited layer, the child will be composited /// into the layer subtree associated with this painting context. Otherwise, @@ -180,6 +201,8 @@ class PaintingContext { /// The current canvas can change whenever you paint a child using this /// context, which means it's fragile to hold a reference to the canvas /// returned by this getter. + /// + /// Only calls within the [canvasBounds] will be recorded. Canvas get canvas { if (_canvas == null) _startRecording(); @@ -188,9 +211,9 @@ class PaintingContext { void _startRecording() { assert(!_isRecording); - _currentLayer = new PictureLayer(); + _currentLayer = new PictureLayer(canvasBounds); _recorder = new ui.PictureRecorder(); - _canvas = new Canvas(_recorder, _paintBounds); + _canvas = new Canvas(_recorder, canvasBounds); _containerLayer.append(_currentLayer); } @@ -203,14 +226,14 @@ class PaintingContext { ..style = PaintingStyle.stroke ..strokeWidth = 6.0 ..color = debugCurrentRepaintColor.toColor(); - canvas.drawRect(_paintBounds.deflate(3.0), paint); + canvas.drawRect(canvasBounds.deflate(3.0), paint); } if (debugPaintLayerBordersEnabled) { final Paint paint = new Paint() ..style = PaintingStyle.stroke ..strokeWidth = 1.0 ..color = debugPaintLayerBordersColor; - canvas.drawRect(_paintBounds, paint); + canvas.drawRect(canvasBounds, paint); } return true; }); @@ -262,7 +285,7 @@ class PaintingContext { } /// Appends the given layer to the recording, and calls the `painter` callback - /// with that layer, providing the [childPaintBounds] as the paint bounds of + /// with that layer, providing the `childPaintBounds` as the paint bounds of /// the child. Canvas recording commands are not guaranteed to be stored /// outside of the paint bounds. /// @@ -272,9 +295,11 @@ class PaintingContext { /// /// The `offset` is the offset to pass to the `painter`. /// - /// If the `childPaintBounds` are not specified then the current layer's + /// If the `childPaintBounds` are not specified then the current layer's paint /// bounds are used. This is appropriate if the child layer does not apply any - /// transformation or clipping to its contents. + /// transformation or clipping to its contents. The `childPaintBounds`, if + /// specified, must be in the coordinate system of the new layer, and should + /// not go outside the current layer's paint bounds. /// /// See also: /// @@ -285,7 +310,7 @@ class PaintingContext { assert(painter != null); _stopRecordingIfNeeded(); _appendLayer(childLayer); - final PaintingContext childContext = new PaintingContext._(childLayer, childPaintBounds ?? _paintBounds); + final PaintingContext childContext = new PaintingContext._(childLayer, childPaintBounds ?? canvasBounds); painter(childContext, offset); childContext._stopRecordingIfNeeded(); } @@ -379,7 +404,7 @@ class PaintingContext { new TransformLayer(transform: effectiveTransform), painter, offset, - childPaintBounds: MatrixUtils.inverseTransformRect(effectiveTransform, _paintBounds), + childPaintBounds: MatrixUtils.inverseTransformRect(effectiveTransform, canvasBounds), ); } else { canvas.save(); @@ -406,6 +431,9 @@ class PaintingContext { void pushOpacity(Offset offset, int alpha, PaintingContextCallback painter) { pushLayer(new OpacityLayer(alpha: alpha), painter, offset); } + + @override + String toString() => '$runtimeType#$hashCode(layer: $_containerLayer, canvas bounds: $canvasBounds)'; } /// An abstract set of layout constraints. @@ -1981,6 +2009,9 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { /// frequently might want to repaint themselves without requiring their parent /// to repaint. /// + /// If this getter returns true, the [paintBounds] are applied to this object + /// and all descendants. + /// /// Warning: This getter must not change value over the lifetime of this object. bool get isRepaintBoundary => false; @@ -2272,12 +2303,15 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { /// The bounds within which this render object will paint. /// - /// A render object is permitted to paint outside the region it occupies - /// during layout but is not permitted to paint outside these paints bounds. - /// These paint bounds are used to construct memory-efficient composited - /// layers, which means attempting to paint outside these bounds can attempt - /// to write to pixels that do not exist in this render object's composited - /// layer. + /// A render object and its descendants are permitted to paint outside the + /// region it occupies during layout, but they are not permitted to paint + /// outside these paints bounds. These paint bounds are used to construct + /// memory-efficient composited layers, which means attempting to paint + /// outside these bounds can attempt to write to pixels that do not exist in + /// this render object's composited layer. + /// + /// The [paintBounds] are only actually enforced when the render object is a + /// repaint boundary; see [isRepaintBoundary]. Rect get paintBounds; /// Override this method to paint debugging information. diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index bb4b7e78c44af..4453c870bbfeb 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -1636,7 +1636,7 @@ class RenderTransform extends RenderProxyBox { Matrix4 inverse; try { inverse = new Matrix4.inverted(_effectiveTransform); - } catch (e) { + } on ArgumentError { // We cannot invert the effective transform. That means the child // doesn't appear on screen and cannot be hit. return false; @@ -1661,7 +1661,6 @@ class RenderTransform extends RenderProxyBox { @override void applyPaintTransform(RenderBox child, Matrix4 transform) { transform.multiply(_effectiveTransform); - super.applyPaintTransform(child, transform); } @override @@ -1785,7 +1784,7 @@ class RenderFittedBox extends RenderProxyBox { Matrix4 inverse; try { inverse = new Matrix4.inverted(_transform); - } catch (e) { + } on ArgumentError { // We cannot invert the effective transform. That means the child // doesn't appear on screen and cannot be hit. return false; @@ -1798,7 +1797,6 @@ class RenderFittedBox extends RenderProxyBox { void applyPaintTransform(RenderBox child, Matrix4 transform) { _updatePaintData(); transform.multiply(_transform); - super.applyPaintTransform(child, transform); } @override @@ -1864,7 +1862,6 @@ class RenderFractionalTranslation extends RenderProxyBox { @override void applyPaintTransform(RenderBox child, Matrix4 transform) { transform.translate(translation.dx * size.width, translation.dy * size.height); - super.applyPaintTransform(child, transform); } @override @@ -3046,3 +3043,194 @@ class RenderExcludeSemantics extends RenderProxyBox { description.add('excluding: $excluding'); } } + +/// Provides an anchor for a [RenderFollowerLayer]. +/// +/// See also: +/// +/// * [CompositedTransformTarget], the corresponding widget. +/// * [LeaderLayer], the layer that this render object creates. +class RenderLeaderLayer extends RenderProxyBox { + /// Creates a render object that uses a [LeaderLayer]. + /// + /// The [link] must not be null. + RenderLeaderLayer({ + @required LayerLink link, + RenderBox child, + }) : assert(link != null), + super(child) { + this.link = link; + } + + /// The link object that connects this [RenderLeaderLayer] with one or more + /// [RenderFollowerLayer]s. + /// + /// This property must not be null. The object must not be associated with + /// another [RenderLeaderLayer] that is also being painted. + LayerLink get link => _link; + LayerLink _link; + set link(LayerLink value) { + assert(value != null); + if (_link == value) + return; + _link = value; + markNeedsPaint(); + } + + @override + bool get alwaysNeedsCompositing => true; + + @override + void paint(PaintingContext context, Offset offset) { + context.pushLayer(new LeaderLayer(link: link, offset: offset), super.paint, Offset.zero); + } + + @override + void debugFillDescription(List description) { + super.debugFillDescription(description); + description.add('link: $link'); + } +} + +/// Transform the child so that its origin is [offset] from the orign of the +/// [RenderLeaderLayer] with the same [LayerLink]. +/// +/// The [RenderLeaderLayer] in question must be earlier in the paint order. +/// +/// Hit testing on descendants of this render object will only work if the +/// target position is within the box that this render object's parent considers +/// to be hitable. +/// +/// See also: +/// +/// * [CompositedTransformFollower], the corresponding widget. +/// * [FollowerLayer], the layer that this render object creates. +class RenderFollowerLayer extends RenderProxyBox { + /// Creates a render object that uses a [FollowerLayer]. + /// + /// The [link] and [offset] arguments must not be null. + RenderFollowerLayer({ + @required LayerLink link, + bool showWhenUnlinked: true, + Offset offset: Offset.zero, + RenderBox child, + }) : assert(link != null), + assert(showWhenUnlinked != null), + assert(offset != null), + super(child) { + this.link = link; + this.showWhenUnlinked = showWhenUnlinked; + this.offset = offset; + } + + /// The link object that connects this [RenderFollowerLayer] with a + /// [RenderLeaderLayer] earlier in the paint order. + LayerLink get link => _link; + LayerLink _link; + set link(LayerLink value) { + assert(value != null); + if (_link == value) + return; + _link = value; + markNeedsPaint(); + } + + /// Whether to show the render object's contents when there is no + /// corresponding [RenderLeaderLayer] with the same [link]. + /// + /// When the render object is linked, the child is positioned such that it has + /// the same global position as the linked [RenderLeaderLayer]. + /// + /// When the render object is not linked, then: if [showWhenUnlinked] is true, + /// the child is visible and not repositioned; if it is false, then child is + /// hidden. + bool get showWhenUnlinked => _showWhenUnlinked; + bool _showWhenUnlinked; + set showWhenUnlinked(bool value) { + assert(value != null); + if (_showWhenUnlinked == value) + return; + _showWhenUnlinked = value; + markNeedsPaint(); + } + + /// The offset to apply to the origin of the linked [RenderLeaderLayer] to + /// obtain this render object's origin. + Offset get offset => _offset; + Offset _offset; + set offset(Offset value) { + assert(value != null); + if (_offset == value) + return; + _offset = value; + markNeedsPaint(); + } + + @override + void detach() { + _layer = null; + super.detach(); + } + + @override + bool get alwaysNeedsCompositing => true; + + /// The layer we created when we were last painted. + FollowerLayer _layer; + + Matrix4 getCurrentTransform() { + return _layer?.getLastTransform() ?? new Matrix4.identity(); + } + + @override + bool hitTest(HitTestResult result, { Offset position }) { + Matrix4 inverse; + try { + inverse = new Matrix4.inverted(getCurrentTransform()); + } on ArgumentError { + // We cannot invert the effective transform. That means the child + // doesn't appear on screen and cannot be hit. + return false; + } + position = MatrixUtils.transformPoint(inverse, position); + return super.hitTest(result, position: position); + } + + @override + void paint(PaintingContext context, Offset offset) { + assert(showWhenUnlinked != null); + _layer = new FollowerLayer( + link: link, + showWhenUnlinked: showWhenUnlinked, + linkedOffset: this.offset, + unlinkedOffset: offset, + ); + context.pushLayer( + _layer, + super.paint, + Offset.zero, + childPaintBounds: new Rect.fromLTRB( + // We don't know where we'll end up, so we have no idea what our cull rect should be. + double.NEGATIVE_INFINITY, + double.NEGATIVE_INFINITY, + double.INFINITY, + double.INFINITY, + ), + ); + } + + @override + void applyPaintTransform(RenderBox child, Matrix4 transform) { + transform.multiply(getCurrentTransform()); + } + + @override + void debugFillDescription(List description) { + super.debugFillDescription(description); + description.add('link: $link'); + description.add('showWhenUnlinked: $showWhenUnlinked'); + description.add('offset: $offset'); + description.add('current transform matrix:'); + description.addAll(debugDescribeTransform(getCurrentTransform())); + } +} diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index bf462de3ff5b4..6444b413d9f65 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -26,6 +26,7 @@ export 'package:flutter/rendering.dart' show FlowPaintingContext, FractionalOffsetTween, HitTestBehavior, + LayerLink, MainAxisAlignment, MainAxisSize, MultiChildLayoutDelegate, @@ -301,11 +302,13 @@ class CustomPaint extends SingleChildRenderObjectWidget { final Size size; @override - RenderCustomPaint createRenderObject(BuildContext context) => new RenderCustomPaint( - painter: painter, - foregroundPainter: foregroundPainter, - preferredSize: size, - ); + RenderCustomPaint createRenderObject(BuildContext context) { + return new RenderCustomPaint( + painter: painter, + foregroundPainter: foregroundPainter, + preferredSize: size, + ); + } @override void updateRenderObject(BuildContext context, RenderCustomPaint renderObject) { @@ -711,12 +714,14 @@ class Transform extends SingleChildRenderObjectWidget { final bool transformHitTests; @override - RenderTransform createRenderObject(BuildContext context) => new RenderTransform( - transform: transform, - origin: origin, - alignment: alignment, - transformHitTests: transformHitTests - ); + RenderTransform createRenderObject(BuildContext context) { + return new RenderTransform( + transform: transform, + origin: origin, + alignment: alignment, + transformHitTests: transformHitTests + ); + } @override void updateRenderObject(BuildContext context, RenderTransform renderObject) { @@ -728,6 +733,140 @@ class Transform extends SingleChildRenderObjectWidget { } } +/// A widget that can be targetted by a [CompositedTransformFollower]. +/// +/// When this widget is composited during the compositing phase (which comes +/// after the paint phase, as described in [WidgetsBinding.drawFrame]), it +/// updates the [link] object so that any [CompositedTransformFollower] widgets +/// that are subsequently composited in the same frame and were given the same +/// [LayerLink] can position themselves at the same screen location. +/// +/// A single [CompositedTransformTarget] can be followed by multiple +/// [CompositedTransformFollower] widgets. +/// +/// The [CompositedTransformTarget] must come earlier in the paint order than +/// any linked [CompositedTransformFollower]s. +/// +/// See also: +/// +/// * [CompositedTransformFollower], the widget that can target this one. +/// * [LeaderLayer], the layer that implements this widget's logic. +class CompositedTransformTarget extends SingleChildRenderObjectWidget { + /// Creates a composited transform target widget. + /// + /// The [link] property must not be null, and must not be currently being used + /// by any other [CompositedTransformTarget] object that is in the tree. + const CompositedTransformTarget({ + Key key, + @required this.link, + Widget child, + }) : assert(link != null), + super(key: key, child: child); + + /// The link object that connects this [CompositedTransformTarget] with one or + /// more [CompositedTransformFollower]s. + /// + /// This property must not be null. The object must not be associated with + /// another [CompositedTransformTarget] that is also being painted. + final LayerLink link; + + @override + RenderLeaderLayer createRenderObject(BuildContext context) { + return new RenderLeaderLayer( + link: link, + ); + } + + @override + void updateRenderObject(BuildContext context, RenderLeaderLayer renderObject) { + renderObject + ..link = link; + } +} + +/// A widget that follows a [CompositedTransformTarget]. +/// +/// When this widget is composited during the compositing phase (which comes +/// after the paint phase, as described in [WidgetsBinding.drawFrame]), it +/// applies a transformation that causes it to provide its child with a +/// coordinate space that matches that of the linked [CompositedTransformTarget] +/// widget, offset by [offset]. +/// +/// The [LayerLink] object used as the [link] must be the same object as that +/// provided to the matching [CompositedTransformTarget]. +/// +/// The [CompositedTransformTarget] must come earlier in the paint order than +/// this [CompositedTransformFollower]. +/// +/// Hit testing on descendants of this widget will only work if the target +/// position is within the box that this widget's parent considers to be +/// hitable. If the parent covers the screen, this is trivially achievable, so +/// this widget is usually used as the root of an [OverlayEntry] in an app-wide +/// [Overlay] (e.g. as created by the [MaterialApp] widget's [Navigator]). +/// +/// See also: +/// +/// * [CompositedTransformTarget], the widget that this widget can target. +/// * [FollowerLayer], the layer that implements this widget's logic. +/// * [Transform], which applies an arbitrary transform to a child. +class CompositedTransformFollower extends SingleChildRenderObjectWidget { + /// Creates a composited transform target widget. + /// + /// The [link] property must not be null. If it was also provided to a + /// [CompositedTransformTarget], that widget must come earlier in the paint + /// order. + /// + /// The [showWhenUnlinked] and [offset] properties must also not be null. + const CompositedTransformFollower({ + Key key, + @required this.link, + this.showWhenUnlinked: true, + this.offset: Offset.zero, + Widget child, + }) : assert(link != null), + assert(showWhenUnlinked != null), + assert(offset != null), + super(key: key, child: child); + + /// The link object that connects this [CompositedTransformFollower] with a + /// [CompositedTransformTarget]. + /// + /// This property must not be null. + final LayerLink link; + + /// Whether to show the widget's contents when there is no corresponding + /// [CompositedTransformTarget] with the same [link]. + /// + /// When the widget is linked, the child is positioned such that it has the + /// same global position as the linked [CompositedTransformTarget]. + /// + /// When the widget is not linked, then: if [showWhenUnlinked] is true, the + /// child is visible and not repositioned; if it is false, then child is + /// hidden. + final bool showWhenUnlinked; + + /// The offset to apply to the origin of the linked + /// [CompositedTransformTarget] to obtain this widget's origin. + final Offset offset; + + @override + RenderFollowerLayer createRenderObject(BuildContext context) { + return new RenderFollowerLayer( + link: link, + showWhenUnlinked: showWhenUnlinked, + offset: offset, + ); + } + + @override + void updateRenderObject(BuildContext context, RenderFollowerLayer renderObject) { + renderObject + ..link = link + ..showWhenUnlinked = showWhenUnlinked + ..offset = offset; + } +} + /// Scales and positions its child within itself according to [fit]. /// /// See also: @@ -1207,9 +1346,11 @@ class SizedBox extends SingleChildRenderObjectWidget { final double height; @override - RenderConstrainedBox createRenderObject(BuildContext context) => new RenderConstrainedBox( - additionalConstraints: _additionalConstraints, - ); + RenderConstrainedBox createRenderObject(BuildContext context) { + return new RenderConstrainedBox( + additionalConstraints: _additionalConstraints, + ); + } BoxConstraints get _additionalConstraints { return new BoxConstraints.tightFor(width: width, height: height); @@ -1353,11 +1494,13 @@ class FractionallySizedBox extends SingleChildRenderObjectWidget { final FractionalOffset alignment; @override - RenderFractionallySizedOverflowBox createRenderObject(BuildContext context) => new RenderFractionallySizedOverflowBox( - alignment: alignment, - widthFactor: widthFactor, - heightFactor: heightFactor - ); + RenderFractionallySizedOverflowBox createRenderObject(BuildContext context) { + return new RenderFractionallySizedOverflowBox( + alignment: alignment, + widthFactor: widthFactor, + heightFactor: heightFactor + ); + } @override void updateRenderObject(BuildContext context, RenderFractionallySizedOverflowBox renderObject) { @@ -1423,10 +1566,12 @@ class LimitedBox extends SingleChildRenderObjectWidget { final double maxHeight; @override - RenderLimitedBox createRenderObject(BuildContext context) => new RenderLimitedBox( - maxWidth: maxWidth, - maxHeight: maxHeight - ); + RenderLimitedBox createRenderObject(BuildContext context) { + return new RenderLimitedBox( + maxWidth: maxWidth, + maxHeight: maxHeight + ); + } @override void updateRenderObject(BuildContext context, RenderLimitedBox renderObject) { @@ -1489,13 +1634,15 @@ class OverflowBox extends SingleChildRenderObjectWidget { final double maxHeight; @override - RenderConstrainedOverflowBox createRenderObject(BuildContext context) => new RenderConstrainedOverflowBox( - alignment: alignment, - minWidth: minWidth, - maxWidth: maxWidth, - minHeight: minHeight, - maxHeight: maxHeight - ); + RenderConstrainedOverflowBox createRenderObject(BuildContext context) { + return new RenderConstrainedOverflowBox( + alignment: alignment, + minWidth: minWidth, + maxWidth: maxWidth, + minHeight: minHeight, + maxHeight: maxHeight + ); + } @override void updateRenderObject(BuildContext context, RenderConstrainedOverflowBox renderObject) { @@ -3196,18 +3343,20 @@ class RawImage extends LeafRenderObjectWidget { final Rect centerSlice; @override - RenderImage createRenderObject(BuildContext context) => new RenderImage( - image: image, - width: width, - height: height, - scale: scale, - color: color, - colorBlendMode: colorBlendMode, - fit: fit, - alignment: alignment, - repeat: repeat, - centerSlice: centerSlice - ); + RenderImage createRenderObject(BuildContext context) { + return new RenderImage( + image: image, + width: width, + height: height, + scale: scale, + color: color, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice + ); + } @override void updateRenderObject(BuildContext context, RenderImage renderObject) { @@ -3371,13 +3520,15 @@ class Listener extends SingleChildRenderObjectWidget { final HitTestBehavior behavior; @override - RenderPointerListener createRenderObject(BuildContext context) => new RenderPointerListener( - onPointerDown: onPointerDown, - onPointerMove: onPointerMove, - onPointerUp: onPointerUp, - onPointerCancel: onPointerCancel, - behavior: behavior - ); + RenderPointerListener createRenderObject(BuildContext context) { + return new RenderPointerListener( + onPointerDown: onPointerDown, + onPointerMove: onPointerMove, + onPointerUp: onPointerUp, + onPointerCancel: onPointerCancel, + behavior: behavior + ); + } @override void updateRenderObject(BuildContext context, RenderPointerListener renderObject) { @@ -3499,10 +3650,12 @@ class IgnorePointer extends SingleChildRenderObjectWidget { final bool ignoringSemantics; @override - RenderIgnorePointer createRenderObject(BuildContext context) => new RenderIgnorePointer( - ignoring: ignoring, - ignoringSemantics: ignoringSemantics - ); + RenderIgnorePointer createRenderObject(BuildContext context) { + return new RenderIgnorePointer( + ignoring: ignoring, + ignoringSemantics: ignoringSemantics + ); + } @override void updateRenderObject(BuildContext context, RenderIgnorePointer renderObject) { @@ -3583,10 +3736,12 @@ class MetaData extends SingleChildRenderObjectWidget { final HitTestBehavior behavior; @override - RenderMetaData createRenderObject(BuildContext context) => new RenderMetaData( - metaData: metaData, - behavior: behavior - ); + RenderMetaData createRenderObject(BuildContext context) { + return new RenderMetaData( + metaData: metaData, + behavior: behavior + ); + } @override void updateRenderObject(BuildContext context, RenderMetaData renderObject) { @@ -3668,12 +3823,14 @@ class Semantics extends SingleChildRenderObjectWidget { final String label; @override - RenderSemanticsAnnotations createRenderObject(BuildContext context) => new RenderSemanticsAnnotations( - container: container, - checked: checked, - selected: selected, - label: label, - ); + RenderSemanticsAnnotations createRenderObject(BuildContext context) { + return new RenderSemanticsAnnotations( + container: container, + checked: checked, + selected: selected, + label: label, + ); + } @override void updateRenderObject(BuildContext context, RenderSemanticsAnnotations renderObject) { diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index eb08c06a9e50c..7ac573f28d76c 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -263,6 +263,7 @@ class EditableTextState extends State implements TextInputClient { TextSelectionOverlay _selectionOverlay; final ScrollController _scrollController = new ScrollController(); + final LayerLink _layerLink = new LayerLink(); bool _didAutoFocus = false; // State lifecycle: @@ -272,6 +273,7 @@ class EditableTextState extends State implements TextInputClient { super.initState(); widget.controller.addListener(_didChangeTextEditingValue); widget.focusNode.addListener(_handleFocusChanged); + _scrollController.addListener(() { _selectionOverlay?.updateForScroll(); }); } @override @@ -436,6 +438,7 @@ class EditableTextState extends State implements TextInputClient { context: context, value: _value, debugRequiredFor: widget, + layerLink: _layerLink, renderObject: renderObject, onSelectionOverlayChanged: _handleSelectionOverlayChanged, selectionControls: widget.selectionControls, @@ -538,19 +541,22 @@ class EditableTextState extends State implements TextInputClient { controller: _scrollController, physics: const ClampingScrollPhysics(), viewportBuilder: (BuildContext context, ViewportOffset offset) { - return new _Editable( - value: _value, - style: widget.style, - cursorColor: widget.cursorColor, - showCursor: _showCursor, - maxLines: widget.maxLines, - selectionColor: widget.selectionColor, - textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0, - textAlign: widget.textAlign, - obscureText: widget.obscureText, - offset: offset, - onSelectionChanged: _handleSelectionChanged, - onCaretChanged: _handleCaretChanged, + return new CompositedTransformTarget( + link: _layerLink, + child: new _Editable( + value: _value, + style: widget.style, + cursorColor: widget.cursorColor, + showCursor: _showCursor, + maxLines: widget.maxLines, + selectionColor: widget.selectionColor, + textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0, + textAlign: widget.textAlign, + obscureText: widget.obscureText, + offset: offset, + onSelectionChanged: _handleSelectionChanged, + onCaretChanged: _handleCaretChanged, + ), ); }, ); diff --git a/packages/flutter/lib/src/widgets/scroll_controller.dart b/packages/flutter/lib/src/widgets/scroll_controller.dart index 4f8b268f0b092..aeb1dea48a0b8 100644 --- a/packages/flutter/lib/src/widgets/scroll_controller.dart +++ b/packages/flutter/lib/src/widgets/scroll_controller.dart @@ -24,6 +24,10 @@ import 'scroll_position_with_single_context.dart'; /// to an individual [Scrollable] widget. To use a custom [ScrollPosition], /// subclass [ScrollController] and override [createScrollPosition]. /// +/// A [ScrollController] is a [Listenable]. It notifies its listeners whenever +/// any of the attached [ScrollPosition]s notify _their_ listeners (i.e. +/// whenever any of them scroll). +/// /// Typically used with [ListView], [GridView], [CustomScrollView]. /// /// See also: diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index d589eb5f34a9f..e5a8846418518 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -73,8 +73,7 @@ abstract class TextSelectionControls { /// Builds a toolbar near a text selection. /// /// Typically displays buttons for copying and pasting text. - // TODO(mpcomplete): A single position is probably insufficient. - Widget buildToolbar(BuildContext context, Offset position, TextSelectionDelegate delegate); + Widget buildToolbar(BuildContext context, Rect globalEditableRegion, Offset position, TextSelectionDelegate delegate); /// Returns the size of the selection handle. Size get handleSize; @@ -92,7 +91,8 @@ class TextSelectionOverlay implements TextSelectionDelegate { @required TextEditingValue value, @required this.context, this.debugRequiredFor, - this.renderObject, + @required this.layerLink, + @required this.renderObject, this.onSelectionOverlayChanged, this.selectionControls, }): assert(value != null), @@ -113,6 +113,10 @@ class TextSelectionOverlay implements TextSelectionDelegate { /// Debugging information for explaining why the [Overlay] is required. final Widget debugRequiredFor; + /// The object supplied to the [CompositedTransformTarget] that wraps the text + /// field. + final LayerLink layerLink; + // TODO(mpcomplete): what if the renderObject is removed or replaced, or // moves? Not sure what cases I need to handle, or how to handle them. /// The editable line in which the selected text is being displayed. @@ -149,8 +153,8 @@ class TextSelectionOverlay implements TextSelectionDelegate { void showHandles() { assert(_handles == null); _handles = [ - new OverlayEntry(builder: (BuildContext c) => _buildHandle(c, _TextSelectionHandlePosition.start)), - new OverlayEntry(builder: (BuildContext c) => _buildHandle(c, _TextSelectionHandlePosition.end)), + new OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.start)), + new OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.end)), ]; Overlay.of(context, debugRequiredFor: debugRequiredFor).insertAll(_handles); _handleController.forward(from: 0.0); @@ -184,6 +188,14 @@ class TextSelectionOverlay implements TextSelectionDelegate { } } + /// Causes the overlay to update its rendering. + /// + /// This is intended to be called when the [renderObject] may have changed its + /// text metrics (e.g. because the text was scrolled). + void updateForScroll() { + _markNeedsBuild(); + } + void _markNeedsBuild([Duration duration]) { if (_handles != null) { _handles[0].markNeedsBuild(); @@ -223,10 +235,11 @@ class TextSelectionOverlay implements TextSelectionDelegate { child: new _TextSelectionHandleOverlay( onSelectionHandleChanged: (TextSelection newSelection) { _handleSelectionHandleChanged(newSelection, position); }, onSelectionHandleTapped: _handleSelectionHandleTapped, + layerLink: layerLink, renderObject: renderObject, selection: _selection, selectionControls: selectionControls, - position: position + position: position, ) ); } @@ -241,12 +254,22 @@ class TextSelectionOverlay implements TextSelectionDelegate { (endpoints.length == 1) ? endpoints[0].point.dx : (endpoints[0].point.dx + endpoints[1].point.dx) / 2.0, - endpoints[0].point.dy - renderObject.size.height + endpoints[0].point.dy - renderObject.size.height, + ); + + final Rect editingRegion = new Rect.fromPoints( + renderObject.localToGlobal(Offset.zero), + renderObject.localToGlobal(renderObject.size.bottomRight(Offset.zero)), ); return new FadeTransition( opacity: _toolbarOpacity, - child: selectionControls.buildToolbar(context, midpoint, this) + child: new CompositedTransformFollower( + link: layerLink, + showWhenUnlinked: false, + offset: -editingRegion.topLeft, + child: selectionControls.buildToolbar(context, editingRegion, midpoint, this), + ), ); } @@ -298,16 +321,18 @@ class TextSelectionOverlay implements TextSelectionDelegate { class _TextSelectionHandleOverlay extends StatefulWidget { const _TextSelectionHandleOverlay({ Key key, - this.selection, - this.position, - this.renderObject, - this.onSelectionHandleChanged, - this.onSelectionHandleTapped, - this.selectionControls + @required this.selection, + @required this.position, + @required this.layerLink, + @required this.renderObject, + @required this.onSelectionHandleChanged, + @required this.onSelectionHandleTapped, + @required this.selectionControls }) : super(key: key); final TextSelection selection; final _TextSelectionHandlePosition position; + final LayerLink layerLink; final RenderEditable renderObject; final ValueChanged onSelectionHandleChanged; final VoidCallback onSelectionHandleTapped; @@ -379,19 +404,23 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay break; } - return new GestureDetector( - onPanStart: _handleDragStart, - onPanUpdate: _handleDragUpdate, - onTap: _handleTap, - child: new Stack( - children: [ - new Positioned( - left: point.dx, - top: point.dy, - child: widget.selectionControls.buildHandle(context, type) - ) - ] - ) + return new CompositedTransformFollower( + link: widget.layerLink, + showWhenUnlinked: false, + child: new GestureDetector( + onPanStart: _handleDragStart, + onPanUpdate: _handleDragUpdate, + onTap: _handleTap, + child: new Stack( + children: [ + new Positioned( + left: point.dx, + top: point.dy, + child: widget.selectionControls.buildHandle(context, type), + ), + ], + ), + ), ); } diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 7382e83bf40bf..530e546e4bf84 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -27,7 +27,7 @@ class MockClipboard { Widget overlay(Widget child) { return new MediaQuery( - data: const MediaQueryData(), + data: const MediaQueryData(size: const Size(800.0, 600.0)), child: new Overlay( initialEntries: [ new OverlayEntry( @@ -73,10 +73,22 @@ void main() { return renderEditable; } + List globalize(Iterable points, RenderBox box) { + return points.map((TextSelectionPoint point) { + return new TextSelectionPoint( + box.localToGlobal(point.point), + point.direction, + ); + }).toList(); + } + Offset textOffsetToPosition(WidgetTester tester, int offset) { final RenderEditable renderEditable = findRenderEditable(tester); - final List endpoints = renderEditable.getEndpointsForSelection( - new TextSelection.collapsed(offset: offset), + final List endpoints = globalize( + renderEditable.getEndpointsForSelection( + new TextSelection.collapsed(offset: offset), + ), + renderEditable, ); expect(endpoints.length, 1); return endpoints[0].point + const Offset(0.0, -2.0); @@ -309,15 +321,19 @@ void main() { await tester.pump(const Duration(seconds: 2)); await gesture.up(); await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero final TextSelection selection = controller.selection; final RenderEditable renderEditable = findRenderEditable(tester); - final List endpoints = renderEditable.getEndpointsForSelection(selection); + final List endpoints = globalize( + renderEditable.getEndpointsForSelection(selection), + renderEditable, + ); expect(endpoints.length, 2); // Drag the right handle 2 letters to the right. - // Note: use a small offset because the endpoint is on the very corner + // We use a small offset because the endpoint is on the very corner // of the handle. Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0); Offset newHandlePos = textOffsetToPosition(tester, selection.extentOffset+2); @@ -368,10 +384,15 @@ void main() { // Tap the selection handle to bring up the "paste / select all" menu. await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); await tester.pumpWidget(builder()); + await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero RenderEditable renderEditable = findRenderEditable(tester); - List endpoints = renderEditable.getEndpointsForSelection(controller.selection); + List endpoints = globalize( + renderEditable.getEndpointsForSelection(controller.selection), + renderEditable, + ); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); await tester.pumpWidget(builder()); + await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero // SELECT ALL should select all the text. await tester.tap(find.text('SELECT ALL')); @@ -388,10 +409,15 @@ void main() { // Tap again to bring back the menu. await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); await tester.pumpWidget(builder()); + await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero renderEditable = findRenderEditable(tester); - endpoints = renderEditable.getEndpointsForSelection(controller.selection); + endpoints = globalize( + renderEditable.getEndpointsForSelection(controller.selection), + renderEditable, + ); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); await tester.pumpWidget(builder()); + await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero // PASTE right before the 'e'. await tester.tap(find.text('PASTE')); @@ -422,8 +448,12 @@ void main() { // Tap the selection handle to bring up the "paste / select all" menu. await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); await tester.pumpWidget(builder()); + await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero final RenderEditable renderEditable = findRenderEditable(tester); - final List endpoints = renderEditable.getEndpointsForSelection(controller.selection); + final List endpoints = globalize( + renderEditable.getEndpointsForSelection(controller.selection), + renderEditable, + ); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); await tester.pumpWidget(builder()); @@ -547,12 +577,16 @@ void main() { await tester.pump(const Duration(seconds: 2)); await gesture.up(); await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero expect(controller.selection.baseOffset, 39); expect(controller.selection.extentOffset, 44); final RenderEditable renderEditable = findRenderEditable(tester); - final List endpoints = renderEditable.getEndpointsForSelection(controller.selection); + final List endpoints = globalize( + renderEditable.getEndpointsForSelection(controller.selection), + renderEditable, + ); expect(endpoints.length, 2); // Drag the right handle to the third line, just after 'Third'. @@ -653,7 +687,10 @@ void main() { await tester.pump(const Duration(seconds: 1)); final RenderEditable renderEditable = findRenderEditable(tester); - final List endpoints = renderEditable.getEndpointsForSelection(controller.selection); + final List endpoints = globalize( + renderEditable.getEndpointsForSelection(controller.selection), + renderEditable, + ); expect(endpoints.length, 2); // Drag the left handle to the first line, just after 'First'. @@ -1410,11 +1447,15 @@ void main() { await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2'))); await tester.pumpWidget(builder()); + await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero final RenderEditable renderEditable = findRenderEditable(tester); - final List endpoints = - renderEditable.getEndpointsForSelection(textController.selection); + final List endpoints = globalize( + renderEditable.getEndpointsForSelection(textController.selection), + renderEditable, + ); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); await tester.pumpWidget(builder()); + await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero Clipboard.setData(const ClipboardData(text: '一4二\n5三6')); await tester.tap(find.text('PASTE')); diff --git a/packages/flutter/test/widgets/composited_transform_test.dart b/packages/flutter/test/widgets/composited_transform_test.dart new file mode 100644 index 0000000000000..e293b8c228314 --- /dev/null +++ b/packages/flutter/test/widgets/composited_transform_test.dart @@ -0,0 +1,166 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +void main() { + testWidgets('Composited transforms - only offsets', (WidgetTester tester) async { + final LayerLink link = new LayerLink(); + final GlobalKey key = new GlobalKey(); + await tester.pumpWidget( + new Stack( + children: [ + new Positioned( + left: 123.0, + top: 456.0, + child: new CompositedTransformTarget( + link: link, + child: new Container(height: 10.0, width: 10.0), + ), + ), + new Positioned( + left: 787.0, + top: 343.0, + child: new CompositedTransformFollower( + link: link, + child: new Container(key: key, height: 10.0, width: 10.0), + ), + ), + ], + ), + ); + final RenderBox box = key.currentContext.findRenderObject(); + expect(box.localToGlobal(Offset.zero), const Offset(123.0, 456.0)); + }); + + testWidgets('Composited transforms - with rotations', (WidgetTester tester) async { + final LayerLink link = new LayerLink(); + final GlobalKey key1 = new GlobalKey(); + final GlobalKey key2 = new GlobalKey(); + await tester.pumpWidget( + new Stack( + children: [ + new Positioned( + top: 123.0, + left: 456.0, + child: new Transform.rotate( + angle: 1.0, // radians + child: new CompositedTransformTarget( + link: link, + child: new Container(key: key1, height: 10.0, width: 10.0), + ), + ), + ), + new Positioned( + top: 787.0, + left: 343.0, + child: new Transform.rotate( + angle: -0.3, // radians + child: new CompositedTransformFollower( + link: link, + child: new Container(key: key2, height: 10.0, width: 10.0), + ), + ), + ), + ], + ), + ); + final RenderBox box1 = key1.currentContext.findRenderObject(); + final RenderBox box2 = key2.currentContext.findRenderObject(); + final Offset position1 = box1.localToGlobal(Offset.zero); + final Offset position2 = box2.localToGlobal(Offset.zero); + expect(position1.dx, moreOrLessEquals(position2.dx)); + expect(position1.dy, moreOrLessEquals(position2.dy)); + }); + + testWidgets('Composited transforms - nested', (WidgetTester tester) async { + final LayerLink link = new LayerLink(); + final GlobalKey key1 = new GlobalKey(); + final GlobalKey key2 = new GlobalKey(); + await tester.pumpWidget( + new Stack( + children: [ + new Positioned( + top: 123.0, + left: 456.0, + child: new Transform.rotate( + angle: 1.0, // radians + child: new CompositedTransformTarget( + link: link, + child: new Container(key: key1, height: 10.0, width: 10.0), + ), + ), + ), + new Positioned( + top: 787.0, + left: 343.0, + child: new Transform.rotate( + angle: -0.3, // radians + child: new Padding( + padding: const EdgeInsets.all(20.0), + child: new CompositedTransformFollower( + link: new LayerLink(), + child: new Transform( + transform: new Matrix4.skew(0.9, 1.1), + child: new Padding( + padding: const EdgeInsets.all(20.0), + child: new CompositedTransformFollower( + link: link, + child: new Container(key: key2, height: 10.0, width: 10.0), + ), + ), + ), + ), + ), + ), + ), + ], + ), + ); + final RenderBox box1 = key1.currentContext.findRenderObject(); + final RenderBox box2 = key2.currentContext.findRenderObject(); + final Offset position1 = box1.localToGlobal(Offset.zero); + final Offset position2 = box2.localToGlobal(Offset.zero); + expect(position1.dx, moreOrLessEquals(position2.dx)); + expect(position1.dy, moreOrLessEquals(position2.dy)); + }); + + testWidgets('Composited transforms - hit testing', (WidgetTester tester) async { + final LayerLink link = new LayerLink(); + final GlobalKey key1 = new GlobalKey(); + final GlobalKey key2 = new GlobalKey(); + final GlobalKey key3 = new GlobalKey(); + bool _tapped = false; + await tester.pumpWidget( + new Stack( + children: [ + new Positioned( + left: 123.0, + top: 456.0, + child: new CompositedTransformTarget( + link: link, + child: new Container(key: key1, height: 10.0, width: 10.0), + ), + ), + new CompositedTransformFollower( + link: link, + child: new GestureDetector( + key: key2, + behavior: HitTestBehavior.opaque, + onTap: () { _tapped = true; }, + child: new Container(key: key3, height: 10.0, width: 10.0), + ), + ), + ], + ), + ); + final RenderBox box2 = key2.currentContext.findRenderObject(); + expect(box2.size, const Size(10.0, 10.0)); + expect(_tapped, isFalse); + await tester.tap(find.byKey(key1)); + expect(_tapped, isTrue); + }); +} From 1be406b1b5d1f8faf54994c0cfb21dc991d84d55 Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Tue, 20 Jun 2017 09:53:01 -0700 Subject: [PATCH 41/51] Move the discovered Java installation to the front of PATH when running sdkmanager (#10846) Fixes https://github.com/flutter/flutter/issues/10703 --- packages/flutter_tools/lib/src/android/android_workflow.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart index c01d5af812295..5b7f99b589e9c 100644 --- a/packages/flutter_tools/lib/src/android/android_workflow.dart +++ b/packages/flutter_tools/lib/src/android/android_workflow.dart @@ -178,7 +178,7 @@ class AndroidWorkflow extends DoctorValidator implements Workflow { final String javaBinary = _findJavaBinary(); if (javaBinary != null) { sdkManagerEnv['PATH'] = - platform.environment['PATH'] + os.pathVarSeparator + fs.path.dirname(javaBinary); + fs.path.dirname(javaBinary) + os.pathVarSeparator + platform.environment['PATH']; } final String sdkManagerPath = fs.path.join( From 7d16a96500eaa60e000c2f7cf6b77d76aec939e2 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 20 Jun 2017 10:47:07 -0700 Subject: [PATCH 42/51] Roll engine to fffe502d437ac7931f08c6cef3e3f71fbd36adaa (#10859) --- bin/internal/engine.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/internal/engine.version b/bin/internal/engine.version index 2482f6f51cc2c..9bdb9b82b64da 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -105cc35af39f80218af8fb521a6dfdad9246a329 +fffe502d437ac7931f08c6cef3e3f71fbd36adaa From 0e1b652d53a943da3a233b466723f45e47b1731c Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 20 Jun 2017 11:09:57 -0700 Subject: [PATCH 43/51] Make Demos in flutter_gallery more accessible (#10832) Remaining known issues are #10831 and #10830. --- .../lib/demo/contacts_demo.dart | 14 +- .../flutter_gallery/lib/demo/pesto_demo.dart | 66 +++++----- .../lib/demo/shrine/shrine_home.dart | 124 +++++++++--------- 3 files changed, 106 insertions(+), 98 deletions(-) diff --git a/examples/flutter_gallery/lib/demo/contacts_demo.dart b/examples/flutter_gallery/lib/demo/contacts_demo.dart index 3cd48317fff3d..9f7cfb95108a6 100644 --- a/examples/flutter_gallery/lib/demo/contacts_demo.dart +++ b/examples/flutter_gallery/lib/demo/contacts_demo.dart @@ -70,12 +70,14 @@ class _ContactItem extends StatelessWidget { ) )); } - return new Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: new Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: rowChildren - ) + return new MergeSemantics( + child: new Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: new Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: rowChildren + ) + ), ); } } diff --git a/examples/flutter_gallery/lib/demo/pesto_demo.dart b/examples/flutter_gallery/lib/demo/pesto_demo.dart index b56232a8be6e1..8bb0809f05db5 100644 --- a/examples/flutter_gallery/lib/demo/pesto_demo.dart +++ b/examples/flutter_gallery/lib/demo/pesto_demo.dart @@ -246,41 +246,43 @@ class RecipeCard extends StatelessWidget { @override Widget build(BuildContext context) { - return new GestureDetector( - onTap: onTap, - child: new Card( - child: new Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - new Hero( - tag: recipe.imagePath, - child: new Image.asset(recipe.imagePath, fit: BoxFit.contain) - ), - new Expanded( - child: new Row( - children: [ - new Padding( - padding: const EdgeInsets.all(16.0), - child: new Image.asset( - recipe.ingredientsImagePath, - width: 48.0, - height: 48.0, + return new MergeSemantics( + child: new GestureDetector( + onTap: onTap, + child: new Card( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new Hero( + tag: recipe.imagePath, + child: new Image.asset(recipe.imagePath, fit: BoxFit.contain) + ), + new Expanded( + child: new Row( + children: [ + new Padding( + padding: const EdgeInsets.all(16.0), + child: new Image.asset( + recipe.ingredientsImagePath, + width: 48.0, + height: 48.0, + ), ), - ), - new Expanded( - child: new Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - new Text(recipe.name, style: titleStyle, softWrap: false, overflow: TextOverflow.ellipsis), - new Text(recipe.author, style: authorStyle), - ], + new Expanded( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new Text(recipe.name, style: titleStyle, softWrap: false, overflow: TextOverflow.ellipsis), + new Text(recipe.author, style: authorStyle), + ], + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), ); diff --git a/examples/flutter_gallery/lib/demo/shrine/shrine_home.dart b/examples/flutter_gallery/lib/demo/shrine/shrine_home.dart index 4383bedc305d5..cca75cc1dc5b6 100644 --- a/examples/flutter_gallery/lib/demo/shrine/shrine_home.dart +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_home.dart @@ -252,39 +252,41 @@ class _Heading extends StatelessWidget { Widget build(BuildContext context) { final Size screenSize = MediaQuery.of(context).size; final ShrineTheme theme = ShrineTheme.of(context); - return new SizedBox( - height: screenSize.width > screenSize.height - ? (screenSize.height - kToolbarHeight) * 0.85 - : (screenSize.height - kToolbarHeight) * 0.70, - child: new Container( - decoration: new BoxDecoration( - color: theme.cardBackgroundColor, - border: new Border(bottom: new BorderSide(color: theme.dividerColor)), - ), - child: new CustomMultiChildLayout( - delegate: new _HeadingLayout(), - children: [ - new LayoutId( - id: _HeadingLayout.price, - child: new _FeaturePriceItem(product: product), - ), - new LayoutId( - id: _HeadingLayout.image, - child: new Image.asset(product.imageAsset, fit: BoxFit.cover), - ), - new LayoutId( - id: _HeadingLayout.title, - child: new Text(product.featureTitle, style: theme.featureTitleStyle), - ), - new LayoutId( - id: _HeadingLayout.description, - child: new Text(product.featureDescription, style: theme.featureStyle), - ), - new LayoutId( - id: _HeadingLayout.vendor, - child: new _VendorItem(vendor: product.vendor), - ), - ], + return new MergeSemantics( + child: new SizedBox( + height: screenSize.width > screenSize.height + ? (screenSize.height - kToolbarHeight) * 0.85 + : (screenSize.height - kToolbarHeight) * 0.70, + child: new Container( + decoration: new BoxDecoration( + color: theme.cardBackgroundColor, + border: new Border(bottom: new BorderSide(color: theme.dividerColor)), + ), + child: new CustomMultiChildLayout( + delegate: new _HeadingLayout(), + children: [ + new LayoutId( + id: _HeadingLayout.price, + child: new _FeaturePriceItem(product: product), + ), + new LayoutId( + id: _HeadingLayout.image, + child: new Image.asset(product.imageAsset, fit: BoxFit.cover), + ), + new LayoutId( + id: _HeadingLayout.title, + child: new Text(product.featureTitle, style: theme.featureTitleStyle), + ), + new LayoutId( + id: _HeadingLayout.description, + child: new Text(product.featureDescription, style: theme.featureStyle), + ), + new LayoutId( + id: _HeadingLayout.vendor, + child: new _VendorItem(vendor: product.vendor), + ), + ], + ), ), ), ); @@ -303,35 +305,37 @@ class _ProductItem extends StatelessWidget { @override Widget build(BuildContext context) { - return new Card( - child: new Stack( - children: [ - new Column( - children: [ - new Align( - alignment: FractionalOffset.centerRight, - child: new _ProductPriceItem(product: product), - ), - new Container( - width: 144.0, - height: 144.0, - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: new Hero( - tag: product.tag, - child: new Image.asset(product.imageAsset, fit: BoxFit.contain), + return new MergeSemantics( + child: new Card( + child: new Stack( + children: [ + new Column( + children: [ + new Align( + alignment: FractionalOffset.centerRight, + child: new _ProductPriceItem(product: product), + ), + new Container( + width: 144.0, + height: 144.0, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: new Hero( + tag: product.tag, + child: new Image.asset(product.imageAsset, fit: BoxFit.contain), + ), ), + new Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: new _VendorItem(vendor: product.vendor), ), - new Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: new _VendorItem(vendor: product.vendor), - ), - ], - ), - new Material( - type: MaterialType.transparency, - child: new InkWell(onTap: onPressed), - ), - ], + ], + ), + new Material( + type: MaterialType.transparency, + child: new InkWell(onTap: onPressed), + ), + ], + ), ), ); } From 5fee9789e2b5fbdaa41a8fca8de4df4e8ed5325d Mon Sep 17 00:00:00 2001 From: jcollins-g Date: Tue, 20 Jun 2017 11:27:37 -0700 Subject: [PATCH 44/51] Add version information to dartdoc footer (#10844) * Add version information to dartdoc footer * Add final to variable declaration * Drop toString() --- dev/tools/dartdoc.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dev/tools/dartdoc.dart b/dev/tools/dartdoc.dart index 74f52814686f2..2bc84eb38b6f6 100644 --- a/dev/tools/dartdoc.dart +++ b/dev/tools/dartdoc.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'package:intl/intl.dart'; import 'package:path/path.dart' as path; +import 'update_versions.dart'; const String kDocRoot = 'dev/docs/doc'; @@ -29,9 +30,12 @@ Future main(List args) async { if (path.basename(Directory.current.path) == 'tools') Directory.current = Directory.current.parent.parent; + final RawVersion version = new RawVersion('VERSION'); + // Create the pubspec.yaml file. final StringBuffer buf = new StringBuffer(''' name: Flutter +version: $version dependencies: '''); for (String package in findPackageNames()) { From 2052741ab3524ba7507b8d6f407ce5fc304900d2 Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Tue, 20 Jun 2017 11:32:40 -0700 Subject: [PATCH 45/51] Use a lookup table to calculate the number of days in a month (#10860) Fixes https://github.com/flutter/flutter/issues/10541 --- .../flutter/lib/src/material/date_picker.dart | 16 +++++++++++++--- .../flutter/test/material/date_picker_test.dart | 9 +++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart index 14cb4552770dc..7fcca7c469cad 100644 --- a/packages/flutter/lib/src/material/date_picker.dart +++ b/packages/flutter/lib/src/material/date_picker.dart @@ -220,14 +220,24 @@ class DayPicker extends StatelessWidget { }).toList(growable: false); } + // Do not use this directly - call getDaysInMonth instead. + static const List _kDaysInMonth = const [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + + static int getDaysInMonth(int year, int month) { + if (month == DateTime.FEBRUARY) { + final bool isLeapYear = (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0); + if (isLeapYear) + return 29; + } + return _kDaysInMonth[month - 1]; + } + @override Widget build(BuildContext context) { final ThemeData themeData = Theme.of(context); final int year = displayedMonth.year; final int month = displayedMonth.month; - // Dart's Date time constructor is very forgiving and will understand - // month 13 as January of the next year. :) - final int daysInMonth = new DateTime(year, month + 1).difference(new DateTime(year, month)).inDays; + final int daysInMonth = getDaysInMonth(year, month); // This assumes a start day of SUNDAY, but could be changed. final int firstWeekday = new DateTime(year, month).weekday % 7; final List labels = []; diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart index 38f1513736bea..a50d0c9334bdd 100644 --- a/packages/flutter/test/material/date_picker_test.dart +++ b/packages/flutter/test/material/date_picker_test.dart @@ -329,4 +329,13 @@ void main() { }); }); }); + + test('days in month', () { + expect(DayPicker.getDaysInMonth(2017, 10), 31); + expect(DayPicker.getDaysInMonth(2017, 6), 30); + expect(DayPicker.getDaysInMonth(2017, 2), 28); + expect(DayPicker.getDaysInMonth(2016, 2), 29); + expect(DayPicker.getDaysInMonth(2000, 2), 29); + expect(DayPicker.getDaysInMonth(1900, 2), 28); + }); } From 2f979914e492a64b4a2f9d20fe0d8c535476ccb4 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 20 Jun 2017 12:43:23 -0700 Subject: [PATCH 46/51] Update templates to include google's maven repository (#10842) Going forward, Android support libraries are published on maven (instead of bundling them with the SDK). Many plugins depend on these. To avoid requiring plugin users to add the maven repository to their app this change adds the repository to the template for `flutter create`. This also bumps the support-annotations dependency to 25.4.0 (which also requires the new maven repository). --- .../templates/create/android-java.tmpl/app/build.gradle.tmpl | 4 ++-- .../templates/create/android-java.tmpl/build.gradle | 3 +++ .../create/android-kotlin.tmpl/app/build.gradle.tmpl | 4 ++-- .../templates/create/android-kotlin.tmpl/build.gradle | 3 +++ .../templates/plugin/android-java.tmpl/build.gradle.tmpl | 3 +++ .../templates/plugin/android-kotlin.tmpl/build.gradle.tmpl | 3 +++ 6 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/flutter_tools/templates/create/android-java.tmpl/app/build.gradle.tmpl b/packages/flutter_tools/templates/create/android-java.tmpl/app/build.gradle.tmpl index a527499badb62..35b0cba109c0b 100644 --- a/packages/flutter_tools/templates/create/android-java.tmpl/app/build.gradle.tmpl +++ b/packages/flutter_tools/templates/create/android-java.tmpl/app/build.gradle.tmpl @@ -24,7 +24,7 @@ android { defaultConfig { testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "{{androidIdentifier}}" } @@ -43,7 +43,7 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' + androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' } diff --git a/packages/flutter_tools/templates/create/android-java.tmpl/build.gradle b/packages/flutter_tools/templates/create/android-java.tmpl/build.gradle index ee5325df808c6..f5004b90712c7 100644 --- a/packages/flutter_tools/templates/create/android-java.tmpl/build.gradle +++ b/packages/flutter_tools/templates/create/android-java.tmpl/build.gradle @@ -11,6 +11,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } diff --git a/packages/flutter_tools/templates/create/android-kotlin.tmpl/app/build.gradle.tmpl b/packages/flutter_tools/templates/create/android-kotlin.tmpl/app/build.gradle.tmpl index 47e62ba6e3a88..1b96050f03888 100644 --- a/packages/flutter_tools/templates/create/android-kotlin.tmpl/app/build.gradle.tmpl +++ b/packages/flutter_tools/templates/create/android-kotlin.tmpl/app/build.gradle.tmpl @@ -29,7 +29,7 @@ android { defaultConfig { testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "{{androidIdentifier}}" } @@ -48,7 +48,7 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' + androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' compile 'org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.2-4' diff --git a/packages/flutter_tools/templates/create/android-kotlin.tmpl/build.gradle b/packages/flutter_tools/templates/create/android-kotlin.tmpl/build.gradle index 6a2fb13cd1b6d..b22b7b7dba140 100644 --- a/packages/flutter_tools/templates/create/android-kotlin.tmpl/build.gradle +++ b/packages/flutter_tools/templates/create/android-kotlin.tmpl/build.gradle @@ -12,6 +12,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } diff --git a/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl index 19c0d3496ac72..a67e4e94021b2 100644 --- a/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl +++ b/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl @@ -14,6 +14,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } diff --git a/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.tmpl index c3001b3698f48..9d3eecd8a0e84 100644 --- a/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.tmpl +++ b/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.tmpl @@ -14,6 +14,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } From f01e9418fcd54ee22ecf011dab59a5239a0cbc3a Mon Sep 17 00:00:00 2001 From: Brian Slesinsky Date: Tue, 20 Jun 2017 14:40:42 -0700 Subject: [PATCH 47/51] change --machine flag for `flutter test` to report test progress as JSON (#10848) (The Flutter plugin will use this to update the UI with test progress.) --- packages/flutter_tools/lib/src/commands/test.dart | 10 ++++++---- packages/flutter_tools/lib/src/test/runner.dart | 5 +++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index a26ea295e2dd2..2f2e58e219367 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -172,8 +172,8 @@ class TestCommand extends FlutterCommand { collector = new CoverageCollector(); } - final bool wantEvents = argResults['machine']; - if (collector != null && wantEvents) { + final bool machine = argResults['machine']; + if (collector != null && machine) { throwToolExit( "The test command doesn't support --machine and coverage together"); } @@ -181,7 +181,7 @@ class TestCommand extends FlutterCommand { TestWatcher watcher; if (collector != null) { watcher = collector; - } else if (wantEvents) { + } else if (machine) { watcher = new EventPrinter(); } @@ -192,7 +192,9 @@ class TestCommand extends FlutterCommand { watcher: watcher, enableObservatory: collector != null || startPaused, startPaused: startPaused, - ipv6: argResults['ipv6']); + ipv6: argResults['ipv6'], + json: machine, + ); if (collector != null) { if (!await _collectCoverageData(collector, mergeCoverageData: argResults['merge-coverage'])) diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart index 80f37e18dcf89..c376313ec99f5 100644 --- a/packages/flutter_tools/lib/src/test/runner.dart +++ b/packages/flutter_tools/lib/src/test/runner.dart @@ -24,6 +24,7 @@ Future runTests( bool enableObservatory: false, bool startPaused: false, bool ipv6: false, + bool json: false, TestWatcher watcher, }) async { // Compute the command-line arguments for package:test. @@ -36,6 +37,10 @@ Future runTests( testArgs.add('--concurrency=1'); } + if (json) { + testArgs.addAll(['-r', 'json']); + } + testArgs.add('--'); testArgs.addAll(testFiles); From 8b888e6a12be8a0cdc346cab83faac59768d4253 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 20 Jun 2017 15:27:00 -0700 Subject: [PATCH 48/51] Apply tempalte changes to examples (#10865) --- dev/benchmarks/complex_layout/android/app/build.gradle | 2 +- dev/benchmarks/complex_layout/android/build.gradle | 3 +++ dev/benchmarks/microbenchmarks/android/app/build.gradle | 2 +- dev/benchmarks/microbenchmarks/android/build.gradle | 3 +++ dev/integration_tests/channels/android/app/build.gradle | 2 +- dev/integration_tests/channels/android/build.gradle | 3 +++ dev/integration_tests/ui/android/app/build.gradle | 2 +- dev/integration_tests/ui/android/build.gradle | 3 +++ dev/manual_tests/android/app/build.gradle | 4 ++-- dev/manual_tests/android/build.gradle | 3 +++ examples/catalog/android/app/build.gradle | 2 +- examples/catalog/android/build.gradle | 3 +++ examples/flutter_gallery/android/app/build.gradle | 4 ++-- examples/flutter_gallery/android/build.gradle | 3 +++ examples/flutter_view/android/app/build.gradle | 2 +- examples/flutter_view/android/build.gradle | 3 +++ examples/hello_world/android/app/build.gradle | 4 ++-- examples/hello_world/android/build.gradle | 3 +++ examples/layers/android/app/build.gradle | 2 +- examples/layers/android/build.gradle | 3 +++ examples/platform_channel/android/app/build.gradle | 2 +- examples/platform_channel/android/build.gradle | 3 +++ examples/platform_view/android/app/build.gradle | 4 ++-- examples/platform_view/android/build.gradle | 3 +++ examples/stocks/android/app/build.gradle | 2 +- examples/stocks/android/build.gradle | 3 +++ 26 files changed, 56 insertions(+), 17 deletions(-) diff --git a/dev/benchmarks/complex_layout/android/app/build.gradle b/dev/benchmarks/complex_layout/android/app/build.gradle index 4a8ab5e563fbf..1e4b757467144 100644 --- a/dev/benchmarks/complex_layout/android/app/build.gradle +++ b/dev/benchmarks/complex_layout/android/app/build.gradle @@ -48,7 +48,7 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' + androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' } diff --git a/dev/benchmarks/complex_layout/android/build.gradle b/dev/benchmarks/complex_layout/android/build.gradle index 3053745fdcb0a..14662e4330ac4 100644 --- a/dev/benchmarks/complex_layout/android/build.gradle +++ b/dev/benchmarks/complex_layout/android/build.gradle @@ -11,6 +11,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } diff --git a/dev/benchmarks/microbenchmarks/android/app/build.gradle b/dev/benchmarks/microbenchmarks/android/app/build.gradle index 114a0ee1094c7..7ecba369c447a 100644 --- a/dev/benchmarks/microbenchmarks/android/app/build.gradle +++ b/dev/benchmarks/microbenchmarks/android/app/build.gradle @@ -40,7 +40,7 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' + androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' } diff --git a/dev/benchmarks/microbenchmarks/android/build.gradle b/dev/benchmarks/microbenchmarks/android/build.gradle index 3053745fdcb0a..14662e4330ac4 100644 --- a/dev/benchmarks/microbenchmarks/android/build.gradle +++ b/dev/benchmarks/microbenchmarks/android/build.gradle @@ -11,6 +11,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } diff --git a/dev/integration_tests/channels/android/app/build.gradle b/dev/integration_tests/channels/android/app/build.gradle index 079d53e47a8c5..4b058ea69c8ae 100644 --- a/dev/integration_tests/channels/android/app/build.gradle +++ b/dev/integration_tests/channels/android/app/build.gradle @@ -48,7 +48,7 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' + androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' } diff --git a/dev/integration_tests/channels/android/build.gradle b/dev/integration_tests/channels/android/build.gradle index ee5325df808c6..f5004b90712c7 100644 --- a/dev/integration_tests/channels/android/build.gradle +++ b/dev/integration_tests/channels/android/build.gradle @@ -11,6 +11,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } diff --git a/dev/integration_tests/ui/android/app/build.gradle b/dev/integration_tests/ui/android/app/build.gradle index 4e2e46b56c2a0..ef18a0c305f92 100644 --- a/dev/integration_tests/ui/android/app/build.gradle +++ b/dev/integration_tests/ui/android/app/build.gradle @@ -40,7 +40,7 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' + androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' } diff --git a/dev/integration_tests/ui/android/build.gradle b/dev/integration_tests/ui/android/build.gradle index ee5325df808c6..f5004b90712c7 100644 --- a/dev/integration_tests/ui/android/build.gradle +++ b/dev/integration_tests/ui/android/build.gradle @@ -11,6 +11,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } diff --git a/dev/manual_tests/android/app/build.gradle b/dev/manual_tests/android/app/build.gradle index df50014433967..57ef0e9041535 100644 --- a/dev/manual_tests/android/app/build.gradle +++ b/dev/manual_tests/android/app/build.gradle @@ -24,7 +24,7 @@ android { defaultConfig { testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.flutter.examples.manual_tests" } @@ -43,7 +43,7 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' + androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' } diff --git a/dev/manual_tests/android/build.gradle b/dev/manual_tests/android/build.gradle index ee5325df808c6..f5004b90712c7 100644 --- a/dev/manual_tests/android/build.gradle +++ b/dev/manual_tests/android/build.gradle @@ -11,6 +11,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } diff --git a/examples/catalog/android/app/build.gradle b/examples/catalog/android/app/build.gradle index 114a0ee1094c7..7ecba369c447a 100644 --- a/examples/catalog/android/app/build.gradle +++ b/examples/catalog/android/app/build.gradle @@ -40,7 +40,7 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' + androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' } diff --git a/examples/catalog/android/build.gradle b/examples/catalog/android/build.gradle index ee5325df808c6..f5004b90712c7 100644 --- a/examples/catalog/android/build.gradle +++ b/examples/catalog/android/build.gradle @@ -11,6 +11,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } diff --git a/examples/flutter_gallery/android/app/build.gradle b/examples/flutter_gallery/android/app/build.gradle index 389b4abcc9fff..a4780038757a0 100644 --- a/examples/flutter_gallery/android/app/build.gradle +++ b/examples/flutter_gallery/android/app/build.gradle @@ -24,7 +24,7 @@ android { defaultConfig { testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.flutter.examples.gallery" } @@ -51,7 +51,7 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' + androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' } diff --git a/examples/flutter_gallery/android/build.gradle b/examples/flutter_gallery/android/build.gradle index ee5325df808c6..f5004b90712c7 100644 --- a/examples/flutter_gallery/android/build.gradle +++ b/examples/flutter_gallery/android/build.gradle @@ -11,6 +11,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } diff --git a/examples/flutter_view/android/app/build.gradle b/examples/flutter_view/android/app/build.gradle index 2e13f67b73aab..1b738a4f3fb3d 100644 --- a/examples/flutter_view/android/app/build.gradle +++ b/examples/flutter_view/android/app/build.gradle @@ -40,7 +40,7 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' + androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' compile 'com.android.support:appcompat-v7:25.0.0' diff --git a/examples/flutter_view/android/build.gradle b/examples/flutter_view/android/build.gradle index ee5325df808c6..f5004b90712c7 100644 --- a/examples/flutter_view/android/build.gradle +++ b/examples/flutter_view/android/build.gradle @@ -11,6 +11,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } diff --git a/examples/hello_world/android/app/build.gradle b/examples/hello_world/android/app/build.gradle index 4916c9c46d8e5..dc36ce3267305 100644 --- a/examples/hello_world/android/app/build.gradle +++ b/examples/hello_world/android/app/build.gradle @@ -24,7 +24,7 @@ android { defaultConfig { testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.flutter.examples.hello_world" } @@ -43,7 +43,7 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' + androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' } diff --git a/examples/hello_world/android/build.gradle b/examples/hello_world/android/build.gradle index ee5325df808c6..f5004b90712c7 100644 --- a/examples/hello_world/android/build.gradle +++ b/examples/hello_world/android/build.gradle @@ -11,6 +11,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } diff --git a/examples/layers/android/app/build.gradle b/examples/layers/android/app/build.gradle index 114a0ee1094c7..7ecba369c447a 100644 --- a/examples/layers/android/app/build.gradle +++ b/examples/layers/android/app/build.gradle @@ -40,7 +40,7 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' + androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' } diff --git a/examples/layers/android/build.gradle b/examples/layers/android/build.gradle index 3053745fdcb0a..14662e4330ac4 100644 --- a/examples/layers/android/build.gradle +++ b/examples/layers/android/build.gradle @@ -11,6 +11,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } diff --git a/examples/platform_channel/android/app/build.gradle b/examples/platform_channel/android/app/build.gradle index 9790cd1675a51..e6b8313a8ff85 100644 --- a/examples/platform_channel/android/app/build.gradle +++ b/examples/platform_channel/android/app/build.gradle @@ -41,7 +41,7 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' + androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' } diff --git a/examples/platform_channel/android/build.gradle b/examples/platform_channel/android/build.gradle index ee5325df808c6..f5004b90712c7 100644 --- a/examples/platform_channel/android/build.gradle +++ b/examples/platform_channel/android/build.gradle @@ -11,6 +11,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } diff --git a/examples/platform_view/android/app/build.gradle b/examples/platform_view/android/app/build.gradle index dbab1622467a0..b3cc1c9a73e0e 100644 --- a/examples/platform_view/android/app/build.gradle +++ b/examples/platform_view/android/app/build.gradle @@ -24,7 +24,7 @@ android { defaultConfig { testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.flutter.examples.platform_view" } @@ -43,7 +43,7 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' + androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' compile 'com.android.support:appcompat-v7:25.0.0' diff --git a/examples/platform_view/android/build.gradle b/examples/platform_view/android/build.gradle index ee5325df808c6..f5004b90712c7 100644 --- a/examples/platform_view/android/build.gradle +++ b/examples/platform_view/android/build.gradle @@ -11,6 +11,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } diff --git a/examples/stocks/android/app/build.gradle b/examples/stocks/android/app/build.gradle index 93f1732f7b3d3..1b9716702eb05 100644 --- a/examples/stocks/android/app/build.gradle +++ b/examples/stocks/android/app/build.gradle @@ -41,7 +41,7 @@ flutter { } dependencies { - androidTestCompile 'com.android.support:support-annotations:25.0.0' + androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' } diff --git a/examples/stocks/android/build.gradle b/examples/stocks/android/build.gradle index ee5325df808c6..f5004b90712c7 100644 --- a/examples/stocks/android/build.gradle +++ b/examples/stocks/android/build.gradle @@ -11,6 +11,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } } From d46e208b98d4af93d7029c3e806d443dd51fd116 Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Tue, 20 Jun 2017 18:13:28 -0700 Subject: [PATCH 49/51] Change all ocurrences of '$runtimeType#$hashCode' to use the idAndType method. (#10871) * Change all instances of '$runtimeType#$hashCode' to use the describeIdentity method. The describeIdentity method generates a shorter description with a consistent length consisting of the runtime type and the a 5 hex character long truncated version of the hash code. --- packages/flutter/lib/src/animation/animation.dart | 2 +- .../lib/src/foundation/change_notifier.dart | 3 ++- .../lib/src/foundation/tree_diagnostics_mixin.dart | 11 ++++++++++- packages/flutter/lib/src/gestures/recognizer.dart | 4 ++-- packages/flutter/lib/src/material/app_bar.dart | 2 +- packages/flutter/lib/src/material/material.dart | 2 +- packages/flutter/lib/src/rendering/box.dart | 2 +- packages/flutter/lib/src/rendering/object.dart | 4 ++-- packages/flutter/lib/src/rendering/proxy_box.dart | 2 +- packages/flutter/lib/src/rendering/semantics.dart | 2 +- .../flutter/lib/src/rendering/viewport_offset.dart | 2 +- packages/flutter/lib/src/scheduler/ticker.dart | 2 +- .../flutter/lib/src/services/asset_bundle.dart | 4 ++-- .../flutter/lib/src/services/image_provider.dart | 2 +- .../flutter/lib/src/widgets/focus_manager.dart | 4 ++-- packages/flutter/lib/src/widgets/framework.dart | 14 +++++++------- packages/flutter/lib/src/widgets/overlay.dart | 2 +- .../flutter/lib/src/widgets/scroll_activity.dart | 12 +++++------- .../flutter/lib/src/widgets/scroll_controller.dart | 2 +- packages/flutter/lib/src/widgets/sliver.dart | 2 +- .../test/foundation/service_extensions_test.dart | 6 +++--- .../foundation/tree_diagnostics_mixin_test.dart | 2 +- .../flutter/test/painting/decoration_test.dart | 2 +- packages/flutter/test/widgets/framework_test.dart | 2 +- .../test/widgets/global_keys_duplicated_test.dart | 9 +++++---- .../test/widgets/image_resolution_test.dart | 2 +- packages/flutter/test/widgets/image_test.dart | 8 ++++---- packages/flutter/test/widgets/table_test.dart | 2 +- 28 files changed, 61 insertions(+), 52 deletions(-) diff --git a/packages/flutter/lib/src/animation/animation.dart b/packages/flutter/lib/src/animation/animation.dart index 1140fe7dbf094..c62749f37ac76 100644 --- a/packages/flutter/lib/src/animation/animation.dart +++ b/packages/flutter/lib/src/animation/animation.dart @@ -81,7 +81,7 @@ abstract class Animation extends Listenable { @override String toString() { - return '$runtimeType#$hashCode(${toStringDetails()})'; + return '${describeIdentity(this)}(${toStringDetails()})'; } /// Provides a string describing the status of this object, but not including diff --git a/packages/flutter/lib/src/foundation/change_notifier.dart b/packages/flutter/lib/src/foundation/change_notifier.dart index 06dd9ea97d4af..f871fb1987394 100644 --- a/packages/flutter/lib/src/foundation/change_notifier.dart +++ b/packages/flutter/lib/src/foundation/change_notifier.dart @@ -7,6 +7,7 @@ import 'package:meta/meta.dart'; import 'assertions.dart'; import 'basic_types.dart'; import 'observer_list.dart'; +import 'print.dart'; /// An object that maintains a list of listeners. abstract class Listenable { @@ -185,5 +186,5 @@ class ValueNotifier extends ChangeNotifier { } @override - String toString() => '$runtimeType#$hashCode($value)'; + String toString() => '${idAndType(this)}($value)'; } diff --git a/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart b/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart index cb69a74f6463e..c279ac37fcaff 100644 --- a/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart +++ b/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart @@ -6,6 +6,15 @@ import 'package:meta/meta.dart'; import 'print.dart'; +/// Returns a 5 character long hexadecimal hash code for [object]. +String shortHash(Object object) { + return object.hashCode.toUnsigned(20).toRadixString(16).padLeft(5, '0'); +} + +/// Returns a summary of [object]'s runtime type and hash code. +String describeIdentity(Object object) => + '${object.runtimeType}#${shortHash(object)}'; + /// A mixin that helps dump string representations of trees. abstract class TreeDiagnosticsMixin { // This class is intended to be used as a mixin, and should not be @@ -20,7 +29,7 @@ abstract class TreeDiagnosticsMixin { /// * [toStringShallow], for a detailed description of the object. /// * [toStringDeep], for a description of the subtree rooted at this object. @override - String toString() => '$runtimeType#$hashCode'; + String toString() => describeIdentity(this); /// Returns a one-line detailed description of the object. /// diff --git a/packages/flutter/lib/src/gestures/recognizer.dart b/packages/flutter/lib/src/gestures/recognizer.dart index c7073cabc9529..6b545b7cdf10f 100644 --- a/packages/flutter/lib/src/gestures/recognizer.dart +++ b/packages/flutter/lib/src/gestures/recognizer.dart @@ -86,7 +86,7 @@ abstract class GestureRecognizer extends GestureArenaMember { } @override - String toString() => '$runtimeType#$hashCode'; + String toString() => describeIdentity(this); } /// Base class for gesture recognizers that can only recognize one @@ -321,5 +321,5 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni } @override - String toString() => '$runtimeType#$hashCode($state)'; + String toString() => '${describeIdentity(this)}($state)'; } diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index aecefbda92a09..eece44cc5f76f 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -604,7 +604,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { @override String toString() { - return '$runtimeType#$hashCode(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)'; + return '${describeIdentity(this)}(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)'; } } diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart index fc92a7e14255c..f041b9f1a0d34 100644 --- a/packages/flutter/lib/src/material/material.dart +++ b/packages/flutter/lib/src/material/material.dart @@ -425,5 +425,5 @@ abstract class InkFeature { void paintFeature(Canvas canvas, Matrix4 transform); @override - String toString() => '$runtimeType#$hashCode'; + String toString() => describeIdentity(this); } diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index 270820fd50812..fcdb7acf24e1f 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -572,7 +572,7 @@ class BoxHitTestEntry extends HitTestEntry { final Offset localPosition; @override - String toString() => '${target.runtimeType}#${target.hashCode}@$localPosition'; + String toString() => '${describeIdentity(target)}@$localPosition'; } /// Parent data used by [RenderBox] and its subclasses. diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 6e248e0600451..beb3adbcc7bcc 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -636,7 +636,7 @@ abstract class _SemanticsFragment { Iterable compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }); @override - String toString() => '$runtimeType#$hashCode'; + String toString() => describeIdentity(this); } /// A SemanticsFragment that doesn't produce any [SemanticsNode]s when compiled. @@ -2686,7 +2686,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { /// Returns a human understandable name. @override String toString() { - String header = '$runtimeType#$hashCode'; + String header = describeIdentity(this); if (_relayoutBoundary != null && _relayoutBoundary != this) { int count = 1; RenderObject target = parent; diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 4453c870bbfeb..38b5bb5011f27 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2034,7 +2034,7 @@ abstract class CustomPainter extends Listenable { bool hitTest(Offset position) => null; @override - String toString() => '$runtimeType#$hashCode(${ _repaint?.toString() ?? "" })'; + String toString() => '${describeIdentity(this)}(${ _repaint?.toString() ?? "" })'; } /// Provides a canvas on which to draw during the paint phase. diff --git a/packages/flutter/lib/src/rendering/semantics.dart b/packages/flutter/lib/src/rendering/semantics.dart index cddd67779efbd..f2bdd7a9e682c 100644 --- a/packages/flutter/lib/src/rendering/semantics.dart +++ b/packages/flutter/lib/src/rendering/semantics.dart @@ -781,5 +781,5 @@ class SemanticsOwner extends ChangeNotifier { } @override - String toString() => '$runtimeType#$hashCode'; + String toString() => describeIdentity(this); } diff --git a/packages/flutter/lib/src/rendering/viewport_offset.dart b/packages/flutter/lib/src/rendering/viewport_offset.dart index 2062af0528902..6b1721e2562ba 100644 --- a/packages/flutter/lib/src/rendering/viewport_offset.dart +++ b/packages/flutter/lib/src/rendering/viewport_offset.dart @@ -170,7 +170,7 @@ abstract class ViewportOffset extends ChangeNotifier { String toString() { final List description = []; debugFillDescription(description); - return '$runtimeType#$hashCode(${description.join(", ")})'; + return '${describeIdentity(this)}(${description.join(", ")})'; } /// Add additional information to the given description for use by [toString]. diff --git a/packages/flutter/lib/src/scheduler/ticker.dart b/packages/flutter/lib/src/scheduler/ticker.dart index ff45d5203d65c..fce31cf165afe 100644 --- a/packages/flutter/lib/src/scheduler/ticker.dart +++ b/packages/flutter/lib/src/scheduler/ticker.dart @@ -419,7 +419,7 @@ class TickerFuture implements Future { } @override - String toString() => '$runtimeType#$hashCode(${ _completed == null ? "active" : _completed ? "complete" : "canceled" })'; + String toString() => '${describeIdentity(this)}(${ _completed == null ? "active" : _completed ? "complete" : "canceled" })'; } /// Exception thrown by [Ticker] objects on the [TickerFuture.orCancel] future diff --git a/packages/flutter/lib/src/services/asset_bundle.dart b/packages/flutter/lib/src/services/asset_bundle.dart index de3074c0bb071..3daff2b94c735 100644 --- a/packages/flutter/lib/src/services/asset_bundle.dart +++ b/packages/flutter/lib/src/services/asset_bundle.dart @@ -79,7 +79,7 @@ abstract class AssetBundle { void evict(String key) { } @override - String toString() => '$runtimeType#$hashCode()'; + String toString() => '${describeIdentity(this)}()'; } /// An [AssetBundle] that loads resources over the network. @@ -133,7 +133,7 @@ class NetworkAssetBundle extends AssetBundle { // should implement evict(). @override - String toString() => '$runtimeType#$hashCode($_baseUrl)'; + String toString() => '${describeIdentity(this)}($_baseUrl)'; } /// An [AssetBundle] that permanently caches string and structured resources diff --git a/packages/flutter/lib/src/services/image_provider.dart b/packages/flutter/lib/src/services/image_provider.dart index 6d4644fd93513..1d470127b67be 100644 --- a/packages/flutter/lib/src/services/image_provider.dart +++ b/packages/flutter/lib/src/services/image_provider.dart @@ -578,7 +578,7 @@ class MemoryImage extends ImageProvider { int get hashCode => hashValues(bytes.hashCode, scale); @override - String toString() => '$runtimeType(${bytes.runtimeType}#${bytes.hashCode}, scale: $scale)'; + String toString() => '$runtimeType(${describeIdentity(bytes)}, scale: $scale)'; } /// Fetches an image from an [AssetBundle], associating it with the given scale. /// diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart index f1529079e3b27..a129f622c9f1d 100644 --- a/packages/flutter/lib/src/widgets/focus_manager.dart +++ b/packages/flutter/lib/src/widgets/focus_manager.dart @@ -94,7 +94,7 @@ class FocusNode extends ChangeNotifier { } @override - String toString() => '$runtimeType#$hashCode${hasFocus ? '(FOCUSED)' : ''}'; + String toString() => '${describeIdentity(this)}${hasFocus ? '(FOCUSED)' : ''}'; } /// An interior node in the focus tree. @@ -446,7 +446,7 @@ class FocusManager { String toString() { final String status = _haveScheduledUpdate ? ' UPDATE SCHEDULED' : ''; final String indent = ' '; - return '$runtimeType#$hashCode$status\n' + return '${describeIdentity(this)}$status\n' '${indent}currentFocus: $_currentFocus\n' '${rootScope.toStringDeep(indent, indent)}'; } diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 2a64d1adda025..38ffdd6c90179 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -112,7 +112,7 @@ class UniqueKey extends LocalKey { UniqueKey(); @override - String toString() => '[#$hashCode]'; + String toString() => '[#${shortHash(this)}]'; } /// A key that takes its identity from the object used as its value. @@ -142,8 +142,8 @@ class ObjectKey extends LocalKey { @override String toString() { if (runtimeType == ObjectKey) - return '[${value.runtimeType}#${value.hashCode}]'; - return '[$runtimeType ${value.runtimeType}#${value.hashCode}]'; + return '[${describeIdentity(value)}]'; + return '[$runtimeType ${describeIdentity(value)}]'; } } @@ -333,8 +333,8 @@ class LabeledGlobalKey> extends GlobalKey { String toString() { final String label = _debugLabel != null ? ' $_debugLabel' : ''; if (runtimeType == LabeledGlobalKey) - return '[GlobalKey#$hashCode$label]'; - return '[$runtimeType#$hashCode$label]'; + return '[GlobalKey#${shortHash(this)}$label]'; + return '[${describeIdentity(this)}$label]'; } } @@ -364,7 +364,7 @@ class GlobalObjectKey> extends GlobalKey { int get hashCode => identityHashCode(value); @override - String toString() => '[$runtimeType ${value.runtimeType}#${value.hashCode}]'; + String toString() => '[$runtimeType ${describeIdentity(value)}]'; } /// This class is a work-around for the "is" operator not accepting a variable value as its right operand @@ -1247,7 +1247,7 @@ abstract class State { String toString() { final List data = []; debugFillDescription(data); - return '$runtimeType#$hashCode(${data.join("; ")})'; + return '${describeIdentity(this)}(${data.join("; ")})'; } /// Add additional information to the given description for use by [toString]. diff --git a/packages/flutter/lib/src/widgets/overlay.dart b/packages/flutter/lib/src/widgets/overlay.dart index e8465c5024a7a..54e5978afa0b7 100644 --- a/packages/flutter/lib/src/widgets/overlay.dart +++ b/packages/flutter/lib/src/widgets/overlay.dart @@ -150,7 +150,7 @@ class OverlayEntry { } @override - String toString() => '$runtimeType#$hashCode(opaque: $opaque; maintainState: $maintainState)'; + String toString() => '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)'; } class _OverlayEntry extends StatefulWidget { diff --git a/packages/flutter/lib/src/widgets/scroll_activity.dart b/packages/flutter/lib/src/widgets/scroll_activity.dart index 596e6ed79039c..4cad08f31004f 100644 --- a/packages/flutter/lib/src/widgets/scroll_activity.dart +++ b/packages/flutter/lib/src/widgets/scroll_activity.dart @@ -125,7 +125,7 @@ abstract class ScrollActivity { } @override - String toString() => '$runtimeType#$hashCode'; + String toString() => describeIdentity(this); } /// A scroll activity that does nothing. @@ -282,9 +282,7 @@ class ScrollDragController implements Drag { dynamic _lastDetails; @override - String toString() { - return '$runtimeType#$hashCode'; - } + String toString() => describeIdentity(this); } /// The activity a scroll view performs when a the user drags their finger @@ -350,7 +348,7 @@ class DragScrollActivity extends ScrollActivity { @override String toString() { - return '$runtimeType#$hashCode($_controller)'; + return '${describeIdentity(this)}($_controller)'; } } @@ -441,7 +439,7 @@ class BallisticScrollActivity extends ScrollActivity { @override String toString() { - return '$runtimeType#$hashCode($_controller)'; + return '${describeIdentity(this)}($_controller)'; } } @@ -526,6 +524,6 @@ class DrivenScrollActivity extends ScrollActivity { @override String toString() { - return '$runtimeType#$hashCode($_controller)'; + return '${describeIdentity(this)}($_controller)'; } } diff --git a/packages/flutter/lib/src/widgets/scroll_controller.dart b/packages/flutter/lib/src/widgets/scroll_controller.dart index aeb1dea48a0b8..b96c5de4e5373 100644 --- a/packages/flutter/lib/src/widgets/scroll_controller.dart +++ b/packages/flutter/lib/src/widgets/scroll_controller.dart @@ -238,7 +238,7 @@ class ScrollController extends ChangeNotifier { String toString() { final List description = []; debugFillDescription(description); - return '$runtimeType#$hashCode(${description.join(", ")})'; + return '${describeIdentity(this)}(${description.join(", ")})'; } /// Add additional information to the given description for use by [toString]. diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart index b5db8f704ceff..f9387d518b2cd 100644 --- a/packages/flutter/lib/src/widgets/sliver.dart +++ b/packages/flutter/lib/src/widgets/sliver.dart @@ -94,7 +94,7 @@ abstract class SliverChildDelegate { String toString() { final List description = []; debugFillDescription(description); - return '$runtimeType#$hashCode(${description.join(", ")})'; + return '${describeIdentity(this)}(${description.join(", ")})'; } /// Add additional information to the given description for use by [toString]. diff --git a/packages/flutter/test/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart index f0bfc9b4650a1..e5ea2fdb054ac 100644 --- a/packages/flutter/test/foundation/service_extensions_test.dart +++ b/packages/flutter/test/foundation/service_extensions_test.dart @@ -143,7 +143,7 @@ void main() { expect(console, [ matches( r'^' - r'RenderView#[0-9]+\n' + r'RenderView#[0-9a-f]{5}\n' r' debug mode enabled - [a-zA-Z]+\n' r' window size: Size\(2400\.0, 1800\.0\) \(in physical pixels\)\n' r' device pixel ratio: 3\.0 \(physical pixels per logical pixel\)\n' @@ -163,8 +163,8 @@ void main() { expect(console, [ matches( r'^' - r'TransformLayer#[0-9]+\n' - r' owner: RenderView#[0-9]+\n' + r'TransformLayer#[0-9a-f]{5}\n' + r' owner: RenderView#[0-9a-f]{5}\n' r' creator: RenderView\n' r' offset: Offset\(0\.0, 0\.0\)\n' r' transform:\n' diff --git a/packages/flutter/test/foundation/tree_diagnostics_mixin_test.dart b/packages/flutter/test/foundation/tree_diagnostics_mixin_test.dart index 1d4593c4856f8..7f5a5758bf214 100644 --- a/packages/flutter/test/foundation/tree_diagnostics_mixin_test.dart +++ b/packages/flutter/test/foundation/tree_diagnostics_mixin_test.dart @@ -41,7 +41,7 @@ void main() { ); final String dump = - tree.toStringDeep().replaceAll(new RegExp(r'#\d+'), '#000'); + tree.toStringDeep().replaceAll(new RegExp(r'#[0-9a-z]{5}'), '#000'); expect(dump, equals('''TestTree#000 ├─child node A: TestTree#000 │ diff --git a/packages/flutter/test/painting/decoration_test.dart b/packages/flutter/test/painting/decoration_test.dart index d1dc57381f8ac..2e587a45064f7 100644 --- a/packages/flutter/test/painting/decoration_test.dart +++ b/packages/flutter/test/painting/decoration_test.dart @@ -75,7 +75,7 @@ class DelayedImageProvider extends ImageProvider { } @override - String toString() => '$runtimeType#$hashCode()'; + String toString() => '${describeIdentity(this)}}()'; } class TestImage extends ui.Image { diff --git a/packages/flutter/test/widgets/framework_test.dart b/packages/flutter/test/widgets/framework_test.dart index d8c3f6b6823cb..e4e2ab71f3247 100644 --- a/packages/flutter/test/widgets/framework_test.dart +++ b/packages/flutter/test/widgets/framework_test.dart @@ -483,7 +483,7 @@ void main() { final MultiChildRenderObjectElement element = key0.currentContext; final String dump = - element.toStringDeep().replaceAll(new RegExp(r'#\d+'), '#000'); + element.toStringDeep().replaceAll(new RegExp(r'#[0-9a-f]{5}'), '#000'); expect(dump, equals('''Column([GlobalKey#000]; renderObject: RenderFlex#000) ├Container() │└LimitedBox(maxWidth: 0.0; maxHeight: 0.0; renderObject: RenderLimitedBox#000 relayoutBoundary=up1) diff --git a/packages/flutter/test/widgets/global_keys_duplicated_test.dart b/packages/flutter/test/widgets/global_keys_duplicated_test.dart index 7d774a0ee1b53..1cea63b9a0ac1 100644 --- a/packages/flutter/test/widgets/global_keys_duplicated_test.dart +++ b/packages/flutter/test/widgets/global_keys_duplicated_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; @@ -19,7 +20,7 @@ void main() { expect(error, isFlutterError); expect(error.toString(), startsWith('Duplicate keys found.\n')); expect(error.toString(), contains('Row')); - expect(error.toString(), contains('[GlobalObjectKey int#${0.hashCode}]')); + expect(error.toString(), contains('[GlobalObjectKey ${describeIdentity(0)}]')); }); testWidgets('GlobalKey children of two nodes', (WidgetTester tester) async { @@ -32,7 +33,7 @@ void main() { expect(error.toString(), startsWith('Multiple widgets used the same GlobalKey.\n')); expect(error.toString(), contains('different widgets that both had the following description')); expect(error.toString(), contains('Container')); - expect(error.toString(), contains('[GlobalObjectKey int#${0.hashCode}]')); + expect(error.toString(), contains('[GlobalObjectKey ${describeIdentity(0)}]')); expect(error.toString(), endsWith('\nA GlobalKey can only be specified on one widget at a time in the widget tree.')); }); @@ -47,7 +48,7 @@ void main() { expect(error.toString(), isNot(contains('different widgets that both had the following description'))); expect(error.toString(), contains('Container()')); expect(error.toString(), contains('Container([<\'x\'>])')); - expect(error.toString(), contains('[GlobalObjectKey int#${0.hashCode}]')); + expect(error.toString(), contains('[GlobalObjectKey ${describeIdentity(0)}]')); expect(error.toString(), endsWith('\nA GlobalKey can only be specified on one widget at a time in the widget tree.')); }); @@ -73,7 +74,7 @@ void main() { // The following line is verifying the grammar is correct in this common case. // We should probably also verify the three other combinations that can be generated... expect(error.toString(), contains('This was determined by noticing that after the widget with the above global key was moved out of its previous parent, that previous parent never updated during this frame, meaning that it either did not update at all or updated before the widget was moved, in either case implying that it still thinks that it should have a child with that global key.')); - expect(error.toString(), contains('[GlobalObjectKey int#0]')); + expect(error.toString(), contains('[GlobalObjectKey ${describeIdentity(0)}]')); expect(error.toString(), contains('Container()')); expect(error.toString(), endsWith('\nA GlobalKey can only be specified on one widget at a time in the widget tree.')); expect(error, isFlutterError); diff --git a/packages/flutter/test/widgets/image_resolution_test.dart b/packages/flutter/test/widgets/image_resolution_test.dart index 03ee3f8b2043e..1048f85c091d0 100644 --- a/packages/flutter/test/widgets/image_resolution_test.dart +++ b/packages/flutter/test/widgets/image_resolution_test.dart @@ -77,7 +77,7 @@ class TestAssetBundle extends CachingAssetBundle { } @override - String toString() => '$runtimeType#$hashCode()'; + String toString() => '${describeIdentity(this)}()'; } class TestAssetImage extends AssetImage { diff --git a/packages/flutter/test/widgets/image_test.dart b/packages/flutter/test/widgets/image_test.dart index d513eacba17b3..ddc193bdefb41 100644 --- a/packages/flutter/test/widgets/image_test.dart +++ b/packages/flutter/test/widgets/image_test.dart @@ -294,12 +294,12 @@ void main() { final TestImageProvider imageProvider = new TestImageProvider(); await tester.pumpWidget(new Image(image: imageProvider)); final State image = tester.state/*State*/(find.byType(Image)); - expect(image.toString(), matches(new RegExp(r'_ImageState#[0-9]+\(stream: ImageStream\(OneFrameImageStreamCompleter; unresolved; 1 listener\); pixels: null\)'))); + expect(image.toString(), matches(new RegExp(r'_ImageState#[0-9a-f]{5}\(stream: ImageStream\(OneFrameImageStreamCompleter; unresolved; 1 listener\); pixels: null\)'))); imageProvider.complete(); await tester.pump(); - expect(image.toString(), matches(new RegExp(r'_ImageState#[0-9]+\(stream: ImageStream\(OneFrameImageStreamCompleter; \[100×100\] @ 1\.0x; 1 listener\); pixels: \[100×100\] @ 1\.0x\)'))); + expect(image.toString(), matches(new RegExp(r'_ImageState#[0-9a-f]{5}\(stream: ImageStream\(OneFrameImageStreamCompleter; \[100×100\] @ 1\.0x; 1 listener\); pixels: \[100×100\] @ 1\.0x\)'))); await tester.pumpWidget(new Container()); - expect(image.toString(), matches(new RegExp(r'_ImageState#[0-9]+\(_StateLifecycle.defunct; not mounted; stream: ImageStream\(OneFrameImageStreamCompleter; \[100×100\] @ 1\.0x; 0 listeners\); pixels: \[100×100\] @ 1\.0x\)'))); + expect(image.toString(), matches(new RegExp(r'_ImageState#[0-9a-f]{5}\(_StateLifecycle.defunct; not mounted; stream: ImageStream\(OneFrameImageStreamCompleter; \[100×100\] @ 1\.0x; 0 listeners\); pixels: \[100×100\] @ 1\.0x\)'))); }); testWidgets('Image.memory control test', (WidgetTester tester) async { @@ -343,7 +343,7 @@ class TestImageProvider extends ImageProvider { } @override - String toString() => '$runtimeType#$hashCode()'; + String toString() => '${describeIdentity(this)}()'; } class TestImage extends ui.Image { diff --git a/packages/flutter/test/widgets/table_test.dart b/packages/flutter/test/widgets/table_test.dart index defb96a62ea73..daaf0c8b28052 100644 --- a/packages/flutter/test/widgets/table_test.dart +++ b/packages/flutter/test/widgets/table_test.dart @@ -524,7 +524,7 @@ void main() { final RenderObjectElement element = key0.currentContext; final String dump = - element.toStringDeep().replaceAll(new RegExp(r'#\d+'), '#000'); + element.toStringDeep().replaceAll(new RegExp(r'#[0-9a-f]{5}'), '#000'); expect(dump, equals('''Table([GlobalKey#000]; renderObject: RenderTable#000) ├Text("A") │└RichText(renderObject: RenderParagraph#000 relayoutBoundary=up1) From ceb814aa647ecd310794a72a7a5c28820ec57a25 Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Tue, 20 Jun 2017 22:14:59 -0700 Subject: [PATCH 50/51] Revert "Change all ocurrences of '$runtimeType#$hashCode' to use the idAndType method. (#10871)" (#10880) This reverts commit d46e208b98d4af93d7029c3e806d443dd51fd116. --- packages/flutter/lib/src/animation/animation.dart | 2 +- .../lib/src/foundation/change_notifier.dart | 3 +-- .../lib/src/foundation/tree_diagnostics_mixin.dart | 11 +---------- packages/flutter/lib/src/gestures/recognizer.dart | 4 ++-- packages/flutter/lib/src/material/app_bar.dart | 2 +- packages/flutter/lib/src/material/material.dart | 2 +- packages/flutter/lib/src/rendering/box.dart | 2 +- packages/flutter/lib/src/rendering/object.dart | 4 ++-- packages/flutter/lib/src/rendering/proxy_box.dart | 2 +- packages/flutter/lib/src/rendering/semantics.dart | 2 +- .../flutter/lib/src/rendering/viewport_offset.dart | 2 +- packages/flutter/lib/src/scheduler/ticker.dart | 2 +- .../flutter/lib/src/services/asset_bundle.dart | 4 ++-- .../flutter/lib/src/services/image_provider.dart | 2 +- .../flutter/lib/src/widgets/focus_manager.dart | 4 ++-- packages/flutter/lib/src/widgets/framework.dart | 14 +++++++------- packages/flutter/lib/src/widgets/overlay.dart | 2 +- .../flutter/lib/src/widgets/scroll_activity.dart | 12 +++++++----- .../flutter/lib/src/widgets/scroll_controller.dart | 2 +- packages/flutter/lib/src/widgets/sliver.dart | 2 +- .../test/foundation/service_extensions_test.dart | 6 +++--- .../foundation/tree_diagnostics_mixin_test.dart | 2 +- .../flutter/test/painting/decoration_test.dart | 2 +- packages/flutter/test/widgets/framework_test.dart | 2 +- .../test/widgets/global_keys_duplicated_test.dart | 9 ++++----- .../test/widgets/image_resolution_test.dart | 2 +- packages/flutter/test/widgets/image_test.dart | 8 ++++---- packages/flutter/test/widgets/table_test.dart | 2 +- 28 files changed, 52 insertions(+), 61 deletions(-) diff --git a/packages/flutter/lib/src/animation/animation.dart b/packages/flutter/lib/src/animation/animation.dart index c62749f37ac76..1140fe7dbf094 100644 --- a/packages/flutter/lib/src/animation/animation.dart +++ b/packages/flutter/lib/src/animation/animation.dart @@ -81,7 +81,7 @@ abstract class Animation extends Listenable { @override String toString() { - return '${describeIdentity(this)}(${toStringDetails()})'; + return '$runtimeType#$hashCode(${toStringDetails()})'; } /// Provides a string describing the status of this object, but not including diff --git a/packages/flutter/lib/src/foundation/change_notifier.dart b/packages/flutter/lib/src/foundation/change_notifier.dart index f871fb1987394..06dd9ea97d4af 100644 --- a/packages/flutter/lib/src/foundation/change_notifier.dart +++ b/packages/flutter/lib/src/foundation/change_notifier.dart @@ -7,7 +7,6 @@ import 'package:meta/meta.dart'; import 'assertions.dart'; import 'basic_types.dart'; import 'observer_list.dart'; -import 'print.dart'; /// An object that maintains a list of listeners. abstract class Listenable { @@ -186,5 +185,5 @@ class ValueNotifier extends ChangeNotifier { } @override - String toString() => '${idAndType(this)}($value)'; + String toString() => '$runtimeType#$hashCode($value)'; } diff --git a/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart b/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart index c279ac37fcaff..cb69a74f6463e 100644 --- a/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart +++ b/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart @@ -6,15 +6,6 @@ import 'package:meta/meta.dart'; import 'print.dart'; -/// Returns a 5 character long hexadecimal hash code for [object]. -String shortHash(Object object) { - return object.hashCode.toUnsigned(20).toRadixString(16).padLeft(5, '0'); -} - -/// Returns a summary of [object]'s runtime type and hash code. -String describeIdentity(Object object) => - '${object.runtimeType}#${shortHash(object)}'; - /// A mixin that helps dump string representations of trees. abstract class TreeDiagnosticsMixin { // This class is intended to be used as a mixin, and should not be @@ -29,7 +20,7 @@ abstract class TreeDiagnosticsMixin { /// * [toStringShallow], for a detailed description of the object. /// * [toStringDeep], for a description of the subtree rooted at this object. @override - String toString() => describeIdentity(this); + String toString() => '$runtimeType#$hashCode'; /// Returns a one-line detailed description of the object. /// diff --git a/packages/flutter/lib/src/gestures/recognizer.dart b/packages/flutter/lib/src/gestures/recognizer.dart index 6b545b7cdf10f..c7073cabc9529 100644 --- a/packages/flutter/lib/src/gestures/recognizer.dart +++ b/packages/flutter/lib/src/gestures/recognizer.dart @@ -86,7 +86,7 @@ abstract class GestureRecognizer extends GestureArenaMember { } @override - String toString() => describeIdentity(this); + String toString() => '$runtimeType#$hashCode'; } /// Base class for gesture recognizers that can only recognize one @@ -321,5 +321,5 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni } @override - String toString() => '${describeIdentity(this)}($state)'; + String toString() => '$runtimeType#$hashCode($state)'; } diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index eece44cc5f76f..aecefbda92a09 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -604,7 +604,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { @override String toString() { - return '${describeIdentity(this)}(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)'; + return '$runtimeType#$hashCode(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)'; } } diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart index f041b9f1a0d34..fc92a7e14255c 100644 --- a/packages/flutter/lib/src/material/material.dart +++ b/packages/flutter/lib/src/material/material.dart @@ -425,5 +425,5 @@ abstract class InkFeature { void paintFeature(Canvas canvas, Matrix4 transform); @override - String toString() => describeIdentity(this); + String toString() => '$runtimeType#$hashCode'; } diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index fcdb7acf24e1f..270820fd50812 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -572,7 +572,7 @@ class BoxHitTestEntry extends HitTestEntry { final Offset localPosition; @override - String toString() => '${describeIdentity(target)}@$localPosition'; + String toString() => '${target.runtimeType}#${target.hashCode}@$localPosition'; } /// Parent data used by [RenderBox] and its subclasses. diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index beb3adbcc7bcc..6e248e0600451 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -636,7 +636,7 @@ abstract class _SemanticsFragment { Iterable compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }); @override - String toString() => describeIdentity(this); + String toString() => '$runtimeType#$hashCode'; } /// A SemanticsFragment that doesn't produce any [SemanticsNode]s when compiled. @@ -2686,7 +2686,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { /// Returns a human understandable name. @override String toString() { - String header = describeIdentity(this); + String header = '$runtimeType#$hashCode'; if (_relayoutBoundary != null && _relayoutBoundary != this) { int count = 1; RenderObject target = parent; diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 38b5bb5011f27..4453c870bbfeb 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2034,7 +2034,7 @@ abstract class CustomPainter extends Listenable { bool hitTest(Offset position) => null; @override - String toString() => '${describeIdentity(this)}(${ _repaint?.toString() ?? "" })'; + String toString() => '$runtimeType#$hashCode(${ _repaint?.toString() ?? "" })'; } /// Provides a canvas on which to draw during the paint phase. diff --git a/packages/flutter/lib/src/rendering/semantics.dart b/packages/flutter/lib/src/rendering/semantics.dart index f2bdd7a9e682c..cddd67779efbd 100644 --- a/packages/flutter/lib/src/rendering/semantics.dart +++ b/packages/flutter/lib/src/rendering/semantics.dart @@ -781,5 +781,5 @@ class SemanticsOwner extends ChangeNotifier { } @override - String toString() => describeIdentity(this); + String toString() => '$runtimeType#$hashCode'; } diff --git a/packages/flutter/lib/src/rendering/viewport_offset.dart b/packages/flutter/lib/src/rendering/viewport_offset.dart index 6b1721e2562ba..2062af0528902 100644 --- a/packages/flutter/lib/src/rendering/viewport_offset.dart +++ b/packages/flutter/lib/src/rendering/viewport_offset.dart @@ -170,7 +170,7 @@ abstract class ViewportOffset extends ChangeNotifier { String toString() { final List description = []; debugFillDescription(description); - return '${describeIdentity(this)}(${description.join(", ")})'; + return '$runtimeType#$hashCode(${description.join(", ")})'; } /// Add additional information to the given description for use by [toString]. diff --git a/packages/flutter/lib/src/scheduler/ticker.dart b/packages/flutter/lib/src/scheduler/ticker.dart index fce31cf165afe..ff45d5203d65c 100644 --- a/packages/flutter/lib/src/scheduler/ticker.dart +++ b/packages/flutter/lib/src/scheduler/ticker.dart @@ -419,7 +419,7 @@ class TickerFuture implements Future { } @override - String toString() => '${describeIdentity(this)}(${ _completed == null ? "active" : _completed ? "complete" : "canceled" })'; + String toString() => '$runtimeType#$hashCode(${ _completed == null ? "active" : _completed ? "complete" : "canceled" })'; } /// Exception thrown by [Ticker] objects on the [TickerFuture.orCancel] future diff --git a/packages/flutter/lib/src/services/asset_bundle.dart b/packages/flutter/lib/src/services/asset_bundle.dart index 3daff2b94c735..de3074c0bb071 100644 --- a/packages/flutter/lib/src/services/asset_bundle.dart +++ b/packages/flutter/lib/src/services/asset_bundle.dart @@ -79,7 +79,7 @@ abstract class AssetBundle { void evict(String key) { } @override - String toString() => '${describeIdentity(this)}()'; + String toString() => '$runtimeType#$hashCode()'; } /// An [AssetBundle] that loads resources over the network. @@ -133,7 +133,7 @@ class NetworkAssetBundle extends AssetBundle { // should implement evict(). @override - String toString() => '${describeIdentity(this)}($_baseUrl)'; + String toString() => '$runtimeType#$hashCode($_baseUrl)'; } /// An [AssetBundle] that permanently caches string and structured resources diff --git a/packages/flutter/lib/src/services/image_provider.dart b/packages/flutter/lib/src/services/image_provider.dart index 1d470127b67be..6d4644fd93513 100644 --- a/packages/flutter/lib/src/services/image_provider.dart +++ b/packages/flutter/lib/src/services/image_provider.dart @@ -578,7 +578,7 @@ class MemoryImage extends ImageProvider { int get hashCode => hashValues(bytes.hashCode, scale); @override - String toString() => '$runtimeType(${describeIdentity(bytes)}, scale: $scale)'; + String toString() => '$runtimeType(${bytes.runtimeType}#${bytes.hashCode}, scale: $scale)'; } /// Fetches an image from an [AssetBundle], associating it with the given scale. /// diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart index a129f622c9f1d..f1529079e3b27 100644 --- a/packages/flutter/lib/src/widgets/focus_manager.dart +++ b/packages/flutter/lib/src/widgets/focus_manager.dart @@ -94,7 +94,7 @@ class FocusNode extends ChangeNotifier { } @override - String toString() => '${describeIdentity(this)}${hasFocus ? '(FOCUSED)' : ''}'; + String toString() => '$runtimeType#$hashCode${hasFocus ? '(FOCUSED)' : ''}'; } /// An interior node in the focus tree. @@ -446,7 +446,7 @@ class FocusManager { String toString() { final String status = _haveScheduledUpdate ? ' UPDATE SCHEDULED' : ''; final String indent = ' '; - return '${describeIdentity(this)}$status\n' + return '$runtimeType#$hashCode$status\n' '${indent}currentFocus: $_currentFocus\n' '${rootScope.toStringDeep(indent, indent)}'; } diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 38ffdd6c90179..2a64d1adda025 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -112,7 +112,7 @@ class UniqueKey extends LocalKey { UniqueKey(); @override - String toString() => '[#${shortHash(this)}]'; + String toString() => '[#$hashCode]'; } /// A key that takes its identity from the object used as its value. @@ -142,8 +142,8 @@ class ObjectKey extends LocalKey { @override String toString() { if (runtimeType == ObjectKey) - return '[${describeIdentity(value)}]'; - return '[$runtimeType ${describeIdentity(value)}]'; + return '[${value.runtimeType}#${value.hashCode}]'; + return '[$runtimeType ${value.runtimeType}#${value.hashCode}]'; } } @@ -333,8 +333,8 @@ class LabeledGlobalKey> extends GlobalKey { String toString() { final String label = _debugLabel != null ? ' $_debugLabel' : ''; if (runtimeType == LabeledGlobalKey) - return '[GlobalKey#${shortHash(this)}$label]'; - return '[${describeIdentity(this)}$label]'; + return '[GlobalKey#$hashCode$label]'; + return '[$runtimeType#$hashCode$label]'; } } @@ -364,7 +364,7 @@ class GlobalObjectKey> extends GlobalKey { int get hashCode => identityHashCode(value); @override - String toString() => '[$runtimeType ${describeIdentity(value)}]'; + String toString() => '[$runtimeType ${value.runtimeType}#${value.hashCode}]'; } /// This class is a work-around for the "is" operator not accepting a variable value as its right operand @@ -1247,7 +1247,7 @@ abstract class State { String toString() { final List data = []; debugFillDescription(data); - return '${describeIdentity(this)}(${data.join("; ")})'; + return '$runtimeType#$hashCode(${data.join("; ")})'; } /// Add additional information to the given description for use by [toString]. diff --git a/packages/flutter/lib/src/widgets/overlay.dart b/packages/flutter/lib/src/widgets/overlay.dart index 54e5978afa0b7..e8465c5024a7a 100644 --- a/packages/flutter/lib/src/widgets/overlay.dart +++ b/packages/flutter/lib/src/widgets/overlay.dart @@ -150,7 +150,7 @@ class OverlayEntry { } @override - String toString() => '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)'; + String toString() => '$runtimeType#$hashCode(opaque: $opaque; maintainState: $maintainState)'; } class _OverlayEntry extends StatefulWidget { diff --git a/packages/flutter/lib/src/widgets/scroll_activity.dart b/packages/flutter/lib/src/widgets/scroll_activity.dart index 4cad08f31004f..596e6ed79039c 100644 --- a/packages/flutter/lib/src/widgets/scroll_activity.dart +++ b/packages/flutter/lib/src/widgets/scroll_activity.dart @@ -125,7 +125,7 @@ abstract class ScrollActivity { } @override - String toString() => describeIdentity(this); + String toString() => '$runtimeType#$hashCode'; } /// A scroll activity that does nothing. @@ -282,7 +282,9 @@ class ScrollDragController implements Drag { dynamic _lastDetails; @override - String toString() => describeIdentity(this); + String toString() { + return '$runtimeType#$hashCode'; + } } /// The activity a scroll view performs when a the user drags their finger @@ -348,7 +350,7 @@ class DragScrollActivity extends ScrollActivity { @override String toString() { - return '${describeIdentity(this)}($_controller)'; + return '$runtimeType#$hashCode($_controller)'; } } @@ -439,7 +441,7 @@ class BallisticScrollActivity extends ScrollActivity { @override String toString() { - return '${describeIdentity(this)}($_controller)'; + return '$runtimeType#$hashCode($_controller)'; } } @@ -524,6 +526,6 @@ class DrivenScrollActivity extends ScrollActivity { @override String toString() { - return '${describeIdentity(this)}($_controller)'; + return '$runtimeType#$hashCode($_controller)'; } } diff --git a/packages/flutter/lib/src/widgets/scroll_controller.dart b/packages/flutter/lib/src/widgets/scroll_controller.dart index b96c5de4e5373..aeb1dea48a0b8 100644 --- a/packages/flutter/lib/src/widgets/scroll_controller.dart +++ b/packages/flutter/lib/src/widgets/scroll_controller.dart @@ -238,7 +238,7 @@ class ScrollController extends ChangeNotifier { String toString() { final List description = []; debugFillDescription(description); - return '${describeIdentity(this)}(${description.join(", ")})'; + return '$runtimeType#$hashCode(${description.join(", ")})'; } /// Add additional information to the given description for use by [toString]. diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart index f9387d518b2cd..b5db8f704ceff 100644 --- a/packages/flutter/lib/src/widgets/sliver.dart +++ b/packages/flutter/lib/src/widgets/sliver.dart @@ -94,7 +94,7 @@ abstract class SliverChildDelegate { String toString() { final List description = []; debugFillDescription(description); - return '${describeIdentity(this)}(${description.join(", ")})'; + return '$runtimeType#$hashCode(${description.join(", ")})'; } /// Add additional information to the given description for use by [toString]. diff --git a/packages/flutter/test/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart index e5ea2fdb054ac..f0bfc9b4650a1 100644 --- a/packages/flutter/test/foundation/service_extensions_test.dart +++ b/packages/flutter/test/foundation/service_extensions_test.dart @@ -143,7 +143,7 @@ void main() { expect(console, [ matches( r'^' - r'RenderView#[0-9a-f]{5}\n' + r'RenderView#[0-9]+\n' r' debug mode enabled - [a-zA-Z]+\n' r' window size: Size\(2400\.0, 1800\.0\) \(in physical pixels\)\n' r' device pixel ratio: 3\.0 \(physical pixels per logical pixel\)\n' @@ -163,8 +163,8 @@ void main() { expect(console, [ matches( r'^' - r'TransformLayer#[0-9a-f]{5}\n' - r' owner: RenderView#[0-9a-f]{5}\n' + r'TransformLayer#[0-9]+\n' + r' owner: RenderView#[0-9]+\n' r' creator: RenderView\n' r' offset: Offset\(0\.0, 0\.0\)\n' r' transform:\n' diff --git a/packages/flutter/test/foundation/tree_diagnostics_mixin_test.dart b/packages/flutter/test/foundation/tree_diagnostics_mixin_test.dart index 7f5a5758bf214..1d4593c4856f8 100644 --- a/packages/flutter/test/foundation/tree_diagnostics_mixin_test.dart +++ b/packages/flutter/test/foundation/tree_diagnostics_mixin_test.dart @@ -41,7 +41,7 @@ void main() { ); final String dump = - tree.toStringDeep().replaceAll(new RegExp(r'#[0-9a-z]{5}'), '#000'); + tree.toStringDeep().replaceAll(new RegExp(r'#\d+'), '#000'); expect(dump, equals('''TestTree#000 ├─child node A: TestTree#000 │ diff --git a/packages/flutter/test/painting/decoration_test.dart b/packages/flutter/test/painting/decoration_test.dart index 2e587a45064f7..d1dc57381f8ac 100644 --- a/packages/flutter/test/painting/decoration_test.dart +++ b/packages/flutter/test/painting/decoration_test.dart @@ -75,7 +75,7 @@ class DelayedImageProvider extends ImageProvider { } @override - String toString() => '${describeIdentity(this)}}()'; + String toString() => '$runtimeType#$hashCode()'; } class TestImage extends ui.Image { diff --git a/packages/flutter/test/widgets/framework_test.dart b/packages/flutter/test/widgets/framework_test.dart index e4e2ab71f3247..d8c3f6b6823cb 100644 --- a/packages/flutter/test/widgets/framework_test.dart +++ b/packages/flutter/test/widgets/framework_test.dart @@ -483,7 +483,7 @@ void main() { final MultiChildRenderObjectElement element = key0.currentContext; final String dump = - element.toStringDeep().replaceAll(new RegExp(r'#[0-9a-f]{5}'), '#000'); + element.toStringDeep().replaceAll(new RegExp(r'#\d+'), '#000'); expect(dump, equals('''Column([GlobalKey#000]; renderObject: RenderFlex#000) ├Container() │└LimitedBox(maxWidth: 0.0; maxHeight: 0.0; renderObject: RenderLimitedBox#000 relayoutBoundary=up1) diff --git a/packages/flutter/test/widgets/global_keys_duplicated_test.dart b/packages/flutter/test/widgets/global_keys_duplicated_test.dart index 1cea63b9a0ac1..7d774a0ee1b53 100644 --- a/packages/flutter/test/widgets/global_keys_duplicated_test.dart +++ b/packages/flutter/test/widgets/global_keys_duplicated_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; @@ -20,7 +19,7 @@ void main() { expect(error, isFlutterError); expect(error.toString(), startsWith('Duplicate keys found.\n')); expect(error.toString(), contains('Row')); - expect(error.toString(), contains('[GlobalObjectKey ${describeIdentity(0)}]')); + expect(error.toString(), contains('[GlobalObjectKey int#${0.hashCode}]')); }); testWidgets('GlobalKey children of two nodes', (WidgetTester tester) async { @@ -33,7 +32,7 @@ void main() { expect(error.toString(), startsWith('Multiple widgets used the same GlobalKey.\n')); expect(error.toString(), contains('different widgets that both had the following description')); expect(error.toString(), contains('Container')); - expect(error.toString(), contains('[GlobalObjectKey ${describeIdentity(0)}]')); + expect(error.toString(), contains('[GlobalObjectKey int#${0.hashCode}]')); expect(error.toString(), endsWith('\nA GlobalKey can only be specified on one widget at a time in the widget tree.')); }); @@ -48,7 +47,7 @@ void main() { expect(error.toString(), isNot(contains('different widgets that both had the following description'))); expect(error.toString(), contains('Container()')); expect(error.toString(), contains('Container([<\'x\'>])')); - expect(error.toString(), contains('[GlobalObjectKey ${describeIdentity(0)}]')); + expect(error.toString(), contains('[GlobalObjectKey int#${0.hashCode}]')); expect(error.toString(), endsWith('\nA GlobalKey can only be specified on one widget at a time in the widget tree.')); }); @@ -74,7 +73,7 @@ void main() { // The following line is verifying the grammar is correct in this common case. // We should probably also verify the three other combinations that can be generated... expect(error.toString(), contains('This was determined by noticing that after the widget with the above global key was moved out of its previous parent, that previous parent never updated during this frame, meaning that it either did not update at all or updated before the widget was moved, in either case implying that it still thinks that it should have a child with that global key.')); - expect(error.toString(), contains('[GlobalObjectKey ${describeIdentity(0)}]')); + expect(error.toString(), contains('[GlobalObjectKey int#0]')); expect(error.toString(), contains('Container()')); expect(error.toString(), endsWith('\nA GlobalKey can only be specified on one widget at a time in the widget tree.')); expect(error, isFlutterError); diff --git a/packages/flutter/test/widgets/image_resolution_test.dart b/packages/flutter/test/widgets/image_resolution_test.dart index 1048f85c091d0..03ee3f8b2043e 100644 --- a/packages/flutter/test/widgets/image_resolution_test.dart +++ b/packages/flutter/test/widgets/image_resolution_test.dart @@ -77,7 +77,7 @@ class TestAssetBundle extends CachingAssetBundle { } @override - String toString() => '${describeIdentity(this)}()'; + String toString() => '$runtimeType#$hashCode()'; } class TestAssetImage extends AssetImage { diff --git a/packages/flutter/test/widgets/image_test.dart b/packages/flutter/test/widgets/image_test.dart index ddc193bdefb41..d513eacba17b3 100644 --- a/packages/flutter/test/widgets/image_test.dart +++ b/packages/flutter/test/widgets/image_test.dart @@ -294,12 +294,12 @@ void main() { final TestImageProvider imageProvider = new TestImageProvider(); await tester.pumpWidget(new Image(image: imageProvider)); final State image = tester.state/*State*/(find.byType(Image)); - expect(image.toString(), matches(new RegExp(r'_ImageState#[0-9a-f]{5}\(stream: ImageStream\(OneFrameImageStreamCompleter; unresolved; 1 listener\); pixels: null\)'))); + expect(image.toString(), matches(new RegExp(r'_ImageState#[0-9]+\(stream: ImageStream\(OneFrameImageStreamCompleter; unresolved; 1 listener\); pixels: null\)'))); imageProvider.complete(); await tester.pump(); - expect(image.toString(), matches(new RegExp(r'_ImageState#[0-9a-f]{5}\(stream: ImageStream\(OneFrameImageStreamCompleter; \[100×100\] @ 1\.0x; 1 listener\); pixels: \[100×100\] @ 1\.0x\)'))); + expect(image.toString(), matches(new RegExp(r'_ImageState#[0-9]+\(stream: ImageStream\(OneFrameImageStreamCompleter; \[100×100\] @ 1\.0x; 1 listener\); pixels: \[100×100\] @ 1\.0x\)'))); await tester.pumpWidget(new Container()); - expect(image.toString(), matches(new RegExp(r'_ImageState#[0-9a-f]{5}\(_StateLifecycle.defunct; not mounted; stream: ImageStream\(OneFrameImageStreamCompleter; \[100×100\] @ 1\.0x; 0 listeners\); pixels: \[100×100\] @ 1\.0x\)'))); + expect(image.toString(), matches(new RegExp(r'_ImageState#[0-9]+\(_StateLifecycle.defunct; not mounted; stream: ImageStream\(OneFrameImageStreamCompleter; \[100×100\] @ 1\.0x; 0 listeners\); pixels: \[100×100\] @ 1\.0x\)'))); }); testWidgets('Image.memory control test', (WidgetTester tester) async { @@ -343,7 +343,7 @@ class TestImageProvider extends ImageProvider { } @override - String toString() => '${describeIdentity(this)}()'; + String toString() => '$runtimeType#$hashCode()'; } class TestImage extends ui.Image { diff --git a/packages/flutter/test/widgets/table_test.dart b/packages/flutter/test/widgets/table_test.dart index daaf0c8b28052..defb96a62ea73 100644 --- a/packages/flutter/test/widgets/table_test.dart +++ b/packages/flutter/test/widgets/table_test.dart @@ -524,7 +524,7 @@ void main() { final RenderObjectElement element = key0.currentContext; final String dump = - element.toStringDeep().replaceAll(new RegExp(r'#[0-9a-f]{5}'), '#000'); + element.toStringDeep().replaceAll(new RegExp(r'#\d+'), '#000'); expect(dump, equals('''Table([GlobalKey#000]; renderObject: RenderTable#000) ├Text("A") │└RichText(renderObject: RenderParagraph#000 relayoutBoundary=up1) From 3f3a36787eaa5396cab8f4e85e3f0ef9d86290fd Mon Sep 17 00:00:00 2001 From: jcollins-g Date: Wed, 21 Jun 2017 08:01:26 -0700 Subject: [PATCH 51/51] Update dartdoc to 0.13.0+1 for flutter (#10863) --- dev/bots/docs.sh | 2 +- dev/tools/dartdoc.dart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dev/bots/docs.sh b/dev/bots/docs.sh index 4d9de3f85bae6..c9a7a415add6c 100755 --- a/dev/bots/docs.sh +++ b/dev/bots/docs.sh @@ -8,7 +8,7 @@ set -e bin/flutter --version # Install dartdoc. -bin/cache/dart-sdk/bin/pub global activate dartdoc 0.12.0 +bin/cache/dart-sdk/bin/pub global activate dartdoc 0.13.0+1 # This script generates a unified doc set, and creates # a custom index.html, placing everything into dev/docs/doc. diff --git a/dev/tools/dartdoc.dart b/dev/tools/dartdoc.dart index 2bc84eb38b6f6..fab1c4e2fc2df 100644 --- a/dev/tools/dartdoc.dart +++ b/dev/tools/dartdoc.dart @@ -35,6 +35,7 @@ Future main(List args) async { // Create the pubspec.yaml file. final StringBuffer buf = new StringBuffer(''' name: Flutter +homepage: https://flutter.io version: $version dependencies: ''');