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/VERSION b/VERSION index 321598098eff4..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-dev +0.0.11-dev diff --git a/bin/internal/engine.version b/bin/internal/engine.version index c63693caabebd..9bdb9b82b64da 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -18fdfb86bb3876fcbb4e1d25e5b2aad0c5cd669f +fffe502d437ac7931f08c6cef3e3f71fbd36adaa 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/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/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/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 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 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/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/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; }; 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/dev/tools/dartdoc.dart b/dev/tools/dartdoc.dart index 74f52814686f2..fab1c4e2fc2df 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,13 @@ 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 +homepage: https://flutter.io +version: $version dependencies: '''); for (String package in findPackageNames()) { 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_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/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/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), + ), + ], + ), ), ); } 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" + } } } 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/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/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; +} 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/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/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/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/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. 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. 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/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 82afc4d3b14e2..270820fd50812 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. @@ -1587,9 +1586,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 +1653,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 +1692,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( @@ -1834,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 @@ -1864,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/debug.dart b/packages/flutter/lib/src/rendering/debug.dart index e605d73e52bd9..6183b4bec16f4 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. @@ -120,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; @@ -161,9 +169,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 +185,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..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), ]; } } @@ -477,8 +479,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/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 6eb23ead8b21a..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. @@ -2312,6 +2346,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/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..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 @@ -2862,6 +2859,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. @@ -3029,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 a4c8e7d0a7b40..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, @@ -460,11 +463,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), + ); + }); } } @@ -536,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/pubspec.yaml b/packages/flutter/pubspec.yaml index 095456c136a82..54df2b4f84bcf 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.31-dev author: Flutter Authors description: A framework for writing Flutter applications homepage: http://flutter.io 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); + }); } diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 7591c5cd70d4e..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( @@ -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); @@ -68,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); @@ -112,8 +129,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 +235,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 +278,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 +313,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')); @@ -308,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); @@ -362,16 +379,20 @@ 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'))); 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')); @@ -382,17 +403,21 @@ 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. 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')); @@ -418,14 +443,17 @@ 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'))); 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()); @@ -532,8 +560,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')); @@ -550,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'. @@ -656,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'. @@ -744,6 +778,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], @@ -1340,16 +1443,19 @@ 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()); + 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')); @@ -1382,8 +1488,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 +1497,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))); 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)); 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], + ); + }); }); } 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); + }); +} diff --git a/packages/flutter_driver/pubspec.yaml b/packages/flutter_driver/pubspec.yaml index 3f293665f6024..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-dev +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/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)); diff --git a/packages/flutter_test/pubspec.yaml b/packages/flutter_test/pubspec.yaml index 42d1db7a8306d..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-dev +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 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 diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index b4dd532fbd59f..8584455ec0c40 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,10 +45,10 @@ class AndroidDevices extends PollingDeviceDiscovery { bool get supportsPlatform => true; @override - bool get canListAnything => doctor.androidWorkflow.canListDevices; + 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/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart index aedfca7cd06ca..5b7f99b589e9c 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'); @@ -175,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( 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/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/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/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/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index 6171abcc88b97..2f2e58e219367 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); @@ -176,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"); } @@ -185,7 +181,7 @@ class TestCommand extends FlutterCommand { TestWatcher watcher; if (collector != null) { watcher = collector; - } else if (wantEvents) { + } else if (machine) { watcher = new EventPrinter(); } @@ -196,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/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/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index d01e42f8b4288..191c390b4b818 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); @@ -263,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 { @@ -284,20 +270,16 @@ 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( - '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' )); } - _validateIntelliJVersion(messages, isWebStorm ? kMinWebStormVersion : kMinIdeaVersion); + _validateIntelliJVersion(messages, kMinIdeaVersion); return new ValidationResult( _hasIssues(messages) ? ValidationType.partial : ValidationType.installed, @@ -310,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) @@ -441,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 { diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 396266fb231d4..1cc5c968c8daf 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,21 +33,16 @@ 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(); + 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'); - _listerPath = _checkForCommand('idevice_id'); - _informerPath = _checkForCommand('ideviceinfo'); _iproxyPath = _checkForCommand('iproxy'); - _debuggerPath = _checkForCommand('idevicedebug'); - _loggerPath = _checkForCommand('idevicesyslog'); - _screenshotPath = _checkForCommand('idevicescreenshot'); _pusherPath = _checkForCommand( 'ios-deploy', 'To copy files to iOS devices, please install ios-deploy. To install, run:\n' @@ -57,28 +52,10 @@ class IOSDevice extends Device { } String _installerPath; - String get installerPath => _installerPath; - - String _listerPath; - String get listerPath => _listerPath; - - String _informerPath; - String get informerPath => _informerPath; - String _iproxyPath; - String get iproxyPath => _iproxyPath; - - String _debuggerPath; - String get debuggerPath => _debuggerPath; - - String _loggerPath; - String get loggerPath => _loggerPath; - - String _screenshotPath; - String get screenshotPath => _screenshotPath; - String _pusherPath; - String get pusherPath => _pusherPath; + + final String _sdkVersion; @override bool get supportsHotMode => true; @@ -96,33 +73,34 @@ class IOSDevice extends Device { @override bool get supportsStartPaused => false; - static List getAttachedDevices() { - if (!doctor.iosWorkflow.hasIDeviceId) + // 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 _getAttachedDeviceIDs()) { - final String name = IOSDevice._getDeviceInfo(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; } - 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 @@ -143,7 +121,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; } @@ -166,7 +144,7 @@ class IOSDevice extends Device { } try { - await runCheckedAsync([installerPath, '-i', iosApp.deviceBundlePath]); + await runCheckedAsync([_installerPath, '-i', iosApp.deviceBundlePath]); return true; } catch (e) { return false; @@ -176,7 +154,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; @@ -331,7 +309,7 @@ class IOSDevice extends Device { Future pushFile(ApplicationPackage app, String localFile, String targetFile) async { if (platform.isMacOS) { runSync([ - pusherPath, + _pusherPath, '-t', '1', '--bundle_id', @@ -351,11 +329,7 @@ class IOSDevice extends Device { Future get targetPlatform async => TargetPlatform.ios; @override - Future get sdkNameAndVersion async => 'iOS $_sdkVersion ($_buildVersion)'; - - String get _sdkVersion => _getDeviceInfo(id, 'ProductVersion'); - - String get _buildVersion => _getDeviceInfo(id, 'BuildVersion'); + Future get sdkNameAndVersion async => 'iOS $_sdkVersion'; @override DeviceLogReader getLogReader({ApplicationPackage app}) { @@ -371,12 +345,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 { @@ -408,7 +380,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); @@ -452,7 +424,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, diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart index 696048b221c86..4e3589d1dcfc8 100644 --- a/packages/flutter_tools/lib/src/ios/ios_workflow.dart +++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart @@ -5,8 +5,8 @@ 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'; @@ -14,7 +14,7 @@ import '../base/version.dart'; import '../doctor.dart'; import 'mac.dart'; -Xcode get xcode => Xcode.instance; +IOSWorkflow get iosWorkflow => context.putIfAbsent(IOSWorkflow, () => new IOSWorkflow()); class IOSWorkflow extends DoctorValidator implements Workflow { IOSWorkflow() : super('iOS toolchain - develop for iOS devices'); @@ -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; @@ -31,21 +31,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']); @@ -151,7 +136,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 16430aa207fab..c13e5e1acb54a 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; @@ -33,6 +33,10 @@ const int kXcodeRequiredVersionMinor = 0; // Homebrew. 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); @@ -45,6 +49,36 @@ 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']); + } + + /// 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 { Xcode() { _eulaSigned = false; @@ -81,9 +115,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; @@ -119,6 +150,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) { @@ -354,8 +392,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 +403,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..92788c6e1e333 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,10 +34,10 @@ 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(); + Future> pollingGetDevices() async => IOSSimulatorUtils.instance.getAttachedDevices(); } class IOSSimulatorUtils { @@ -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) { @@ -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,14 +115,12 @@ class SimControl { return getDevices().where((SimDevice device) => device.isBooted).toList(); } - 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 +140,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 +227,7 @@ class IOSSimulator extends Device { @override Future isAppInstalled(ApplicationPackage app) { - return SimControl.instance.isInstalled(app.id); + return SimControl.instance.isInstalled(id, app.id); } @override @@ -474,7 +361,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 +416,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) => @@ -587,8 +474,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 @@ -596,7 +482,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/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( 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); @@ -55,8 +60,8 @@ Future 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)); 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 de68f74d76606..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 @@ -209,10 +219,12 @@ class FlutterVersion { if (beenAWhileSinceWarningWasPrinted && installationSeemsOutdated && await newerFrameworkVersionAvailable()) { printStatus(versionOutOfDateMessage(frameworkAge), emphasis: true); - stamp.store( - newTimeWarningWasPrinted: _clock.now(), - ); - await new Future.delayed(kPauseToLetUserReadTheMessage); + await Future.wait(>[ + stamp.store( + newTimeWarningWasPrinted: _clock.now(), + ), + new Future.delayed(kPauseToLetUserReadTheMessage), + ]); } } @@ -254,7 +266,7 @@ class FlutterVersion { try { final String branch = _channel == 'alpha' ? 'alpha' : 'master'; final DateTime remoteFrameworkCommitDate = DateTime.parse(await FlutterVersion.fetchRemoteFrameworkCommitDate(branch)); - versionCheckStamp.store( + await versionCheckStamp.store( newTimeVersionWasChecked: _clock.now(), newKnownRemoteVersion: remoteFrameworkCommitDate, ); 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" + } } } 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, + }); + }); +} 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); } 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, + }); + }); }); } diff --git a/packages/flutter_tools/test/ios/devices_test.dart b/packages/flutter_tools/test/ios/devices_test.dart index 96ef415e42d38..bb29b0897faf4 100644 --- a/packages/flutter_tools/test/ios/devices_test.dart +++ b/packages/flutter_tools/test/ios/devices_test.dart @@ -3,11 +3,11 @@ // 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'; +import 'package:flutter_tools/src/ios/mac.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; import 'package:process/process.dart'; @@ -16,91 +16,57 @@ import 'package:test/test.dart'; import '../src/context.dart'; class MockProcessManager extends Mock implements ProcessManager {} +class MockXcode extends Mock implements Xcode {} class MockFile extends Mock implements File {} void main() { final FakePlatform osx = new FakePlatform.fromPlatform(const LocalPlatform()); osx.operatingSystem = 'macos'; - group('test screenshot', () { - MockProcessManager mockProcessManager; - MockFile mockOutputFile; - IOSDevice iosDeviceUnderTest; + group('getAttachedDevices', () { + MockXcode mockXcode; setUp(() { - mockProcessManager = new MockProcessManager(); - mockOutputFile = new MockFile(); + mockXcode = new MockXcode(); }); - 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, '', '') - ); - - 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('return no devices if Xcode is not installed', () async { + when(mockXcode.isInstalled).thenReturn(false); + expect(await IOSDevice.getAttachedDevices(), isEmpty); + }, overrides: { + Xcode: () => mockXcode, + }); - 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('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(); + expect(devices, isEmpty); + }, overrides: { + Xcode: () => mockXcode, + }); - 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} - ); + 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(); + 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, + }); }); + } diff --git a/packages/flutter_tools/test/ios/ios_workflow_test.dart b/packages/flutter_tools/test/ios/ios_workflow_test.dart index c7afec7076802..65fd9ec0a890a 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); @@ -163,15 +201,11 @@ 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: { FileSystem: () => fs, + IMobileDevice: () => iMobileDevice, Xcode: () => xcode, ProcessManager: () => processManager, }); @@ -183,17 +217,13 @@ 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(); expect(result.type, ValidationType.installed); }, overrides: { FileSystem: () => fs, + IMobileDevice: () => iMobileDevice, Xcode: () => xcode, ProcessManager: () => processManager, }); @@ -207,6 +237,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 +251,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 +268,6 @@ class IOSWorkflowTestTarget extends IOSWorkflow { @override final bool hasHomebrew; - @override - final Future hasWorkingLibimobiledevice; - @override final Future hasIosDeploy; 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; 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'), ], 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;