diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 9012d3e4fc..51e354d56b 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -58,7 +58,7 @@ class IOSDevices extends PollingDeviceDiscovery { ); } - return await _xcdevice.getAvailableTetheredIOSDevices(timeout: timeout); + return await _xcdevice.getAvailableIOSDevices(timeout: timeout); } @override @@ -73,11 +73,18 @@ class IOSDevices extends PollingDeviceDiscovery { } } +enum IOSDeviceInterface { + none, + usb, + network, +} + class IOSDevice extends Device { IOSDevice(String id, { @required FileSystem fileSystem, @required this.name, @required this.cpuArchitecture, + @required this.interfaceType, @required String sdkVersion, @required Platform platform, @required Artifacts artifacts, @@ -123,16 +130,21 @@ class IOSDevice extends Device { } @override - bool get supportsHotReload => true; + bool get supportsHotReload => interfaceType == IOSDeviceInterface.usb; @override - bool get supportsHotRestart => true; + bool get supportsHotRestart => interfaceType == IOSDeviceInterface.usb; + + @override + bool get supportsFlutterExit => interfaceType == IOSDeviceInterface.usb; @override final String name; final DarwinArch cpuArchitecture; + final IOSDeviceInterface interfaceType; + Map _logReaders; DevicePortForwarder _portForwarder; @@ -178,6 +190,7 @@ class IOSDevice extends Device { deviceId: id, bundlePath: bundle.path, launchArguments: [], + interfaceType: interfaceType, ); } on ProcessException catch (e) { _logger.printError(e.message); @@ -319,6 +332,7 @@ class IOSDevice extends Device { deviceId: id, bundlePath: bundle.path, launchArguments: launchArguments, + interfaceType: interfaceType, ); if (installationResult != 0) { _logger.printError('Could not run ${bundle.path} on $id.'); @@ -410,7 +424,7 @@ class IOSDevice extends Device { void clearLogs() { } @override - bool get supportsScreenshot => _iMobileDevice.isInstalled; + bool get supportsScreenshot => _iMobileDevice.isInstalled && interfaceType == IOSDeviceInterface.usb; @override Future takeScreenshot(File outputFile) async { diff --git a/packages/flutter_tools/lib/src/ios/ios_deploy.dart b/packages/flutter_tools/lib/src/ios/ios_deploy.dart index 246f26375d..fa977550d9 100644 --- a/packages/flutter_tools/lib/src/ios/ios_deploy.dart +++ b/packages/flutter_tools/lib/src/ios/ios_deploy.dart @@ -14,6 +14,7 @@ import '../base/process.dart'; import '../build_info.dart'; import '../cache.dart'; import 'code_signing.dart'; +import 'devices.dart'; // Error message patterns from ios-deploy output const String noProvisioningProfileErrorOne = 'Error 0xe8008015'; @@ -84,6 +85,7 @@ class IOSDeploy { @required String deviceId, @required String bundlePath, @required ListlaunchArguments, + @required IOSDeviceInterface interfaceType, }) async { final List launchCommand = [ _binaryPath, @@ -91,7 +93,8 @@ class IOSDeploy { deviceId, '--bundle', bundlePath, - '--no-wifi', + if (interfaceType != IOSDeviceInterface.network) + '--no-wifi', if (launchArguments.isNotEmpty) ...[ '--args', launchArguments.join(' '), @@ -113,6 +116,7 @@ class IOSDeploy { @required String deviceId, @required String bundlePath, @required List launchArguments, + @required IOSDeviceInterface interfaceType, }) async { final List launchCommand = [ _binaryPath, @@ -120,7 +124,8 @@ class IOSDeploy { deviceId, '--bundle', bundlePath, - '--no-wifi', + if (interfaceType != IOSDeviceInterface.network) + '--no-wifi', '--justlaunch', if (launchArguments.isNotEmpty) ...[ '--args', diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart index e7c64109f6..ceb0eb554a 100644 --- a/packages/flutter_tools/lib/src/macos/xcode.dart +++ b/packages/flutter_tools/lib/src/macos/xcode.dart @@ -290,7 +290,7 @@ class XCDevice { List _cachedListResults; /// [timeout] defaults to 2 seconds. - Future> getAvailableTetheredIOSDevices({ Duration timeout }) async { + Future> getAvailableIOSDevices({ Duration timeout }) async { final List allAvailableDevices = await _getAllDevices(timeout: timeout ?? const Duration(seconds: 2)); if (allAvailableDevices == null) { @@ -364,8 +364,11 @@ class XCDevice { } } + final IOSDeviceInterface interface = _interfaceType(deviceProperties); + // Only support USB devices, skip "network" interface (Xcode > Window > Devices and Simulators > Connect via network). - if (!_isUSBTethered(deviceProperties)) { + // TODO(jmagman): Remove this check once wirelessly detected devices can be observed and attached, https://github.com/flutter/flutter/issues/15072. + if (interface != IOSDeviceInterface.usb) { continue; } @@ -373,6 +376,7 @@ class XCDevice { device['identifier'] as String, name: device['name'] as String, cpuArchitecture: _cpuArchitecture(deviceProperties), + interfaceType: interface, sdkVersion: _sdkVersion(deviceProperties), artifacts: globals.artifacts, fileSystem: globals.fs, @@ -409,10 +413,18 @@ class XCDevice { return null; } - static bool _isUSBTethered(Map deviceProperties) { - // Interface can be "usb", "network", or not present for simulators. - return deviceProperties.containsKey('interface') && - (deviceProperties['interface'] as String).toLowerCase() == 'usb'; + static IOSDeviceInterface _interfaceType(Map deviceProperties) { + // Interface can be "usb", "network", or "none" for simulators + // and unknown future interfaces. + if (deviceProperties.containsKey('interface')) { + if ((deviceProperties['interface'] as String).toLowerCase() == 'network') { + return IOSDeviceInterface.network; + } else { + return IOSDeviceInterface.usb; + } + } + + return IOSDeviceInterface.none; } static String _sdkVersion(Map deviceProperties) { diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart index 18e421df4a..ba4d78d0a2 100644 --- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart @@ -72,7 +72,8 @@ void main() { iMobileDevice: iMobileDevice, name: 'iPhone 1', sdkVersion: '13.3', - cpuArchitecture: DarwinArch.arm64 + cpuArchitecture: DarwinArch.arm64, + interfaceType: IOSDeviceInterface.usb, ); }); @@ -87,7 +88,8 @@ void main() { iMobileDevice: iMobileDevice, name: 'iPhone 1', cpuArchitecture: DarwinArch.arm64, - sdkVersion: '1.0.0' + sdkVersion: '1.0.0', + interfaceType: IOSDeviceInterface.usb, ).majorSdkVersion, 1); expect(IOSDevice( 'device-123', @@ -99,7 +101,8 @@ void main() { iMobileDevice: iMobileDevice, name: 'iPhone 1', cpuArchitecture: DarwinArch.arm64, - sdkVersion: '13.1.1' + sdkVersion: '13.1.1', + interfaceType: IOSDeviceInterface.usb, ).majorSdkVersion, 13); expect(IOSDevice( 'device-123', @@ -111,7 +114,8 @@ void main() { iMobileDevice: iMobileDevice, name: 'iPhone 1', cpuArchitecture: DarwinArch.arm64, - sdkVersion: '10' + sdkVersion: '10', + interfaceType: IOSDeviceInterface.usb, ).majorSdkVersion, 10); expect(IOSDevice( 'device-123', @@ -123,7 +127,8 @@ void main() { iMobileDevice: iMobileDevice, name: 'iPhone 1', cpuArchitecture: DarwinArch.arm64, - sdkVersion: '0' + sdkVersion: '0', + interfaceType: IOSDeviceInterface.usb, ).majorSdkVersion, 0); expect(IOSDevice( 'device-123', @@ -135,7 +140,8 @@ void main() { iMobileDevice: iMobileDevice, name: 'iPhone 1', cpuArchitecture: DarwinArch.arm64, - sdkVersion: 'bogus' + sdkVersion: 'bogus', + interfaceType: IOSDeviceInterface.usb, ).majorSdkVersion, 0); }); @@ -154,6 +160,7 @@ void main() { name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64, + interfaceType: IOSDeviceInterface.usb, ); }, throwsAssertionError, @@ -237,6 +244,7 @@ void main() { name: 'iPhone 1', sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64, + interfaceType: IOSDeviceInterface.usb, ); logReader1 = createLogReader(device, appPackage1, mockProcess1); logReader2 = createLogReader(device, appPackage2, mockProcess2); @@ -321,8 +329,9 @@ void main() { logger: logger, platform: macPlatform, fileSystem: MemoryFileSystem.test(), + interfaceType: IOSDeviceInterface.usb, ); - when(mockXcdevice.getAvailableTetheredIOSDevices()) + when(mockXcdevice.getAvailableIOSDevices()) .thenAnswer((Invocation invocation) => Future>.value([device])); final List devices = await iosDevices.pollingGetDevices(); diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart index ab1a8d5dc8..d8e9214113 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart @@ -25,7 +25,7 @@ const Map kDyLdLibEntry = { }; void main() { - testWithoutContext('IOSDevice.installApp calls ios-deploy correctly', () async { + testWithoutContext('IOSDevice.installApp calls ios-deploy correctly with USB', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -47,6 +47,36 @@ void main() { final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, + interfaceType: IOSDeviceInterface.usb, + ); + final bool wasInstalled = await device.installApp(iosApp); + + expect(wasInstalled, true); + expect(processManager.hasRemainingExpectations, false); + }); + + testWithoutContext('IOSDevice.installApp calls ios-deploy correctly with network', () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleDir: fileSystem.currentDirectory, + ); + final FakeProcessManager processManager = FakeProcessManager.list([ + const FakeCommand(command: [ + 'ios-deploy', + '--id', + '1234', + '--bundle', + '/', + ], environment: { + 'PATH': '/usr/bin:null', + ...kDyLdLibEntry, + }) + ]); + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + interfaceType: IOSDeviceInterface.network, ); final bool wasInstalled = await device.installApp(iosApp); @@ -237,6 +267,7 @@ IOSDevice setUpIOSDevice({ @required ProcessManager processManager, FileSystem fileSystem, Logger logger, + IOSDeviceInterface interfaceType, }) { logger ??= BufferLogger.test(); final FakePlatform platform = FakePlatform( @@ -270,6 +301,7 @@ IOSDevice setUpIOSDevice({ cache: cache, ), artifacts: artifacts, + interfaceType: interfaceType, ); } diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart index 8c9b1d0a0d..d09ead76bd 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart @@ -89,6 +89,7 @@ IOSDevice setUpIOSDevice(FileSystem fileSystem) { sdkVersion: '13.3', cpuArchitecture: DarwinArch.arm64, artifacts: artifacts, + interfaceType: IOSDeviceInterface.usb, ); } diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart index 50e6641fe2..b6a768a1fd 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart @@ -329,6 +329,7 @@ IOSDevice setUpIOSDevice({ cache: cache, ), cpuArchitecture: DarwinArch.arm64, + interfaceType: IOSDeviceInterface.usb, ); } diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart index 683abc81cc..71c859afaa 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart @@ -398,6 +398,7 @@ IOSDevice setUpIOSDevice({ cache: cache, ), cpuArchitecture: DarwinArch.arm64, + interfaceType: IOSDeviceInterface.usb, ); } diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart index 935f34d76b..b60f9a200c 100644 --- a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart @@ -99,7 +99,7 @@ void main() { when(processManager.run(['xcrun', 'xcdevice', 'list', '--timeout', '2'])) .thenThrow(const ProcessException('xcrun', ['xcdevice', 'list', '--timeout', '2'])); - expect(await xcdevice.getAvailableTetheredIOSDevices(), isEmpty); + expect(await xcdevice.getAvailableIOSDevices(), isEmpty); }); testWithoutContext('diagnostics xcdevice fails', () async { @@ -359,7 +359,7 @@ void main() { testWithoutContext('Xcode not installed', () async { when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(false); - expect(await xcdevice.getAvailableTetheredIOSDevices(), isEmpty); + expect(await xcdevice.getAvailableIOSDevices(), isEmpty); }); testUsingContext('returns devices', () async { @@ -466,7 +466,7 @@ void main() { command: ['xcrun', 'xcdevice', 'list', '--timeout', '2'], stdout: devicesOutput, )); - final List devices = await xcdevice.getAvailableTetheredIOSDevices(); + final List devices = await xcdevice.getAvailableIOSDevices(); expect(devices, hasLength(3)); expect(devices[0].id, 'd83d5bc53967baa0ee18626ba87b6254b2ab5418'); expect(devices[0].name, 'An iPhone (Space Gray)'); @@ -496,7 +496,7 @@ void main() { command: ['xcrun', 'xcdevice', 'list', '--timeout', '20'], stdout: '[]', )); - await xcdevice.getAvailableTetheredIOSDevices(timeout: const Duration(seconds: 20)); + await xcdevice.getAvailableIOSDevices(timeout: const Duration(seconds: 20)); expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); @@ -535,7 +535,7 @@ void main() { command: ['xcrun', 'xcdevice', 'list', '--timeout', '2'], stdout: devicesOutput, )); - final List devices = await xcdevice.getAvailableTetheredIOSDevices(); + final List devices = await xcdevice.getAvailableIOSDevices(); expect(devices, hasLength(1)); expect(devices[0].id, '43ad2fda7991b34fe1acbda82f9e2fd3d6ddc9f7'); expect(fakeProcessManager.hasRemainingExpectations, isFalse); @@ -583,7 +583,7 @@ void main() { command: ['xcrun', 'xcdevice', 'list', '--timeout', '2'], stdout: devicesOutput, )); - final List devices = await xcdevice.getAvailableTetheredIOSDevices(); + final List devices = await xcdevice.getAvailableIOSDevices(); expect(devices[0].cpuArchitecture, DarwinArch.armv7); expect(devices[1].cpuArchitecture, DarwinArch.arm64); expect(fakeProcessManager.hasRemainingExpectations, isFalse); @@ -634,7 +634,7 @@ void main() { stdout: devicesOutput, )); - await xcdevice.getAvailableTetheredIOSDevices(); + await xcdevice.getAvailableIOSDevices(); final List errors = await xcdevice.getDiagnostics(); expect(errors, hasLength(1)); expect(fakeProcessManager.hasRemainingExpectations, isFalse);