diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 2663d34f1aa..15b718ab85a 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -267,6 +267,7 @@ class IOSDevice extends Device { required this.cpuArchitecture, required this.connectionInterface, required this.isConnected, + required this.isPaired, required this.devModeEnabled, required this.isCoreDevice, String? sdkVersion, @@ -336,6 +337,11 @@ class IOSDevice extends Device { @override bool isConnected; + bool devModeEnabled = false; + + /// Device has trusted this computer and paired. + bool isPaired = false; + /// CoreDevice is a device connectivity stack introduced in Xcode 15. Devices /// with iOS 17 or greater are CoreDevices. final bool isCoreDevice; @@ -344,8 +350,6 @@ class IOSDevice extends Device { DevicePortForwarder? _portForwarder; - bool devModeEnabled = false; - @visibleForTesting IOSDeployDebugger? iosDeployDebugger; diff --git a/packages/flutter_tools/lib/src/macos/xcdevice.dart b/packages/flutter_tools/lib/src/macos/xcdevice.dart index 226b47c5c69..f122b6efd5a 100644 --- a/packages/flutter_tools/lib/src/macos/xcdevice.dart +++ b/packages/flutter_tools/lib/src/macos/xcdevice.dart @@ -544,6 +544,7 @@ class XCDevice { } bool devModeEnabled = true; bool isConnected = true; + bool isPaired = true; final Map? errorProperties = _errorProperties(device); if (errorProperties != null) { final String? errorMessage = _parseErrorMessage(errorProperties); @@ -564,6 +565,10 @@ class XCDevice { if (code != -10) { isConnected = false; } + // Error: iPhone is not paired with your computer. To use iPhone with Xcode, unlock it and choose to trust this computer when prompted. (code -9) + if (code == -9) { + isPaired = false; + } if (code == 6) { devModeEnabled = false; @@ -635,6 +640,7 @@ class XCDevice { xcodeDebug: _xcodeDebug, platform: globals.platform, devModeEnabled: devModeEnabled, + isPaired: isPaired, isCoreDevice: coreDevice != null, ); } diff --git a/packages/flutter_tools/lib/src/runner/target_devices.dart b/packages/flutter_tools/lib/src/runner/target_devices.dart index 7a149a4f4b8..a639096bd93 100644 --- a/packages/flutter_tools/lib/src/runner/target_devices.dart +++ b/packages/flutter_tools/lib/src/runner/target_devices.dart @@ -30,6 +30,8 @@ String _noMatchingDeviceMessage(String deviceId) => 'No supported devices found "matching '$deviceId'."; String flutterSpecifiedDeviceDevModeDisabled(String deviceName) => 'To use ' "'$deviceName' for development, enable Developer Mode in Settings → Privacy & Security."; +String flutterSpecifiedDeviceUnpaired(String deviceName) => "'$deviceName' is not paired. " + 'Open Xcode and trust this computer when prompted.'; /// This class handles functionality of finding and selecting target devices. /// @@ -494,17 +496,24 @@ class TargetDevicesWithExtendedWirelessDeviceDiscovery extends TargetDevices { if (specifiedDevices.length == 1) { Device? matchedDevice = specifiedDevices.first; - // If the only matching device does not have Developer Mode enabled, - // print a warning - if (matchedDevice is IOSDevice && !matchedDevice.devModeEnabled) { - _logger.printStatus( - flutterSpecifiedDeviceDevModeDisabled(matchedDevice.name) - ); - return null; - } + if (matchedDevice is IOSDevice) { + // If the only matching device is not paired, print a warning + if (!matchedDevice.isPaired) { + _logger.printStatus(flutterSpecifiedDeviceUnpaired(matchedDevice.name)); + return null; + } + // If the only matching device does not have Developer Mode enabled, + // print a warning + if (!matchedDevice.devModeEnabled) { + _logger.printStatus( + flutterSpecifiedDeviceDevModeDisabled(matchedDevice.name) + ); + return null; + } - if (!matchedDevice.isConnected && matchedDevice is IOSDevice) { - matchedDevice = await _waitForIOSDeviceToConnect(matchedDevice); + if (!matchedDevice.isConnected) { + matchedDevice = await _waitForIOSDeviceToConnect(matchedDevice); + } } if (matchedDevice != null && matchedDevice.isConnected) { @@ -512,9 +521,14 @@ class TargetDevicesWithExtendedWirelessDeviceDiscovery extends TargetDevices { } } else { - for (final Device device in specifiedDevices) { + for (final IOSDevice device in specifiedDevices.whereType()) { + // Print warning for every matching unpaired device. + if (!device.isPaired) { + _logger.printStatus(flutterSpecifiedDeviceUnpaired(device.name)); + } + // Print warning for every matching device that does not have Developer Mode enabled. - if (device is IOSDevice && !device.devModeEnabled) { + if (!device.devModeEnabled) { _logger.printStatus( flutterSpecifiedDeviceDevModeDisabled(device.name) ); 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 146e05b2b70..9b3593f67c8 100644 --- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart @@ -85,6 +85,7 @@ void main() { cpuArchitecture: DarwinArch.arm64, connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ); @@ -106,6 +107,7 @@ void main() { cpuArchitecture: DarwinArch.armv7, connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ); @@ -128,6 +130,7 @@ void main() { sdkVersion: '1.0.0', connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ).majorSdkVersion, 1); @@ -146,6 +149,7 @@ void main() { sdkVersion: '13.1.1', connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ).majorSdkVersion, 13); @@ -164,6 +168,7 @@ void main() { sdkVersion: '10', connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ).majorSdkVersion, 10); @@ -182,6 +187,7 @@ void main() { sdkVersion: '0', connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ).majorSdkVersion, 0); @@ -200,6 +206,7 @@ void main() { sdkVersion: 'bogus', connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ).majorSdkVersion, 0); @@ -221,6 +228,7 @@ void main() { sdkVersion: '13.3.1', connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ).sdkVersion; @@ -244,6 +252,7 @@ void main() { sdkVersion: '13.3.1 (20ADBC)', connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ).sdkVersion; @@ -267,6 +276,7 @@ void main() { sdkVersion: '16.4.1(a) (20ADBC)', connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ).sdkVersion; @@ -290,6 +300,7 @@ void main() { sdkVersion: '0', connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ).sdkVersion; @@ -312,6 +323,7 @@ void main() { cpuArchitecture: DarwinArch.arm64, connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ).sdkVersion; @@ -332,6 +344,7 @@ void main() { sdkVersion: 'bogus', connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ).sdkVersion; @@ -354,6 +367,7 @@ void main() { cpuArchitecture: DarwinArch.arm64, connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ); @@ -377,6 +391,7 @@ void main() { cpuArchitecture: DarwinArch.arm64, connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ); @@ -406,6 +421,7 @@ void main() { cpuArchitecture: DarwinArch.arm64, connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ); @@ -501,6 +517,7 @@ void main() { cpuArchitecture: DarwinArch.arm64, connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ); @@ -571,6 +588,7 @@ void main() { fileSystem: MemoryFileSystem.test(), connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ); @@ -590,6 +608,7 @@ void main() { fileSystem: MemoryFileSystem.test(), connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ); @@ -889,6 +908,7 @@ void main() { fileSystem: MemoryFileSystem.test(), connectionInterface: DeviceConnectionInterface.attached, isConnected: false, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ); 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 6a5f5226fdb..d703c7f0574 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 @@ -434,6 +434,7 @@ IOSDevice setUpIOSDevice({ iProxy: IProxy.test(logger: logger, processManager: processManager), connectionInterface: interfaceType ?? DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: isCoreDevice, ); 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 abe1e850a4a..e1aaffa755d 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 @@ -106,6 +106,7 @@ IOSDevice setUpIOSDevice(FileSystem fileSystem) { iProxy: IProxy.test(logger: logger, processManager: processManager), connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: false, ); 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 9814d599a09..1eb72b6eee2 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 @@ -869,6 +869,7 @@ IOSDevice setUpIOSDevice({ cpuArchitecture: DarwinArch.arm64, connectionInterface: DeviceConnectionInterface.attached, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: isCoreDevice, ); 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 0325f551727..8539678157d 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 @@ -1094,6 +1094,7 @@ IOSDevice setUpIOSDevice({ cpuArchitecture: DarwinArch.arm64, connectionInterface: interfaceType, isConnected: true, + isPaired: true, devModeEnabled: true, isCoreDevice: isCoreDevice, ); diff --git a/packages/flutter_tools/test/general.shard/runner/target_devices_test.dart b/packages/flutter_tools/test/general.shard/runner/target_devices_test.dart index 35a3015fb01..5e5add9670d 100644 --- a/packages/flutter_tools/test/general.shard/runner/target_devices_test.dart +++ b/packages/flutter_tools/test/general.shard/runner/target_devices_test.dart @@ -1275,7 +1275,7 @@ To use 'target-device' for development, enable Developer Mode in Settings → Pr testUsingContext('when one of the matching devices has dev mode disabled', () async { deviceManager.iosDiscoverer.deviceList = [FakeIOSDevice(deviceName: 'target-device-1', devModeEnabled: false, isConnected: false), - FakeIOSDevice(deviceName: 'target-device-2', devModeEnabled: true)]; + FakeIOSDevice(deviceName: 'target-device-2')]; final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -1296,6 +1296,45 @@ To use 'target-device-1' for development, enable Developer Mode in Settings → To use 'target-device-2' for development, enable Developer Mode in Settings → Privacy & Security. No devices found yet. Checking for wireless devices... +No supported devices found with name or id matching 'target-device'. +''')); + expect(devices, isNull); + }); + + testUsingContext('when only matching device is unpaired', () async { + deviceManager.iosDiscoverer.deviceList = [FakeIOSDevice(deviceName: 'target-device', isPaired: false)]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +'target-device' is not paired. Open Xcode and trust this computer when prompted. +''')); + expect(devices, isNull); + }); + + testUsingContext('when one of the matching devices is unpaired', () async { + deviceManager.iosDiscoverer.deviceList = [FakeIOSDevice(deviceName: 'target-device-1', isPaired: false, isConnected: false), + FakeIOSDevice(deviceName: 'target-device-2')]; + + final List? devices = await targetDevices.findAllTargetDevices(); + expect(logger.statusText, contains(''' +'target-device-1' is not paired. Open Xcode and trust this computer when prompted. +Checking for wireless devices... +''')); + expect(devices, isNotNull); + }); + + testUsingContext('when all matching devices are unpaired', () async { + deviceManager.iosDiscoverer.deviceList = [FakeIOSDevice(deviceName: 'target-device-1', isPaired: false, isConnected: false), + FakeIOSDevice(deviceName: 'target-device-2', isPaired: false, isConnected: false)]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, contains(''' +'target-device-1' is not paired. Open Xcode and trust this computer when prompted. +'target-device-2' is not paired. Open Xcode and trust this computer when prompted. +No devices found yet. Checking for wireless devices... + No supported devices found with name or id matching 'target-device'. ''')); expect(devices, isNull); @@ -2777,16 +2816,16 @@ class FakeIOSDevice extends Fake implements IOSDevice { FakeIOSDevice({ String? deviceId, String? deviceName, - bool? devModeEnabled, bool deviceSupported = true, bool deviceSupportForProject = true, this.ephemeral = true, this.isConnected = true, + this.devModeEnabled = true, + this.isPaired = true, this.platformType = PlatformType.ios, this.connectionInterface = DeviceConnectionInterface.attached, }) : id = deviceId ?? 'xxx', name = deviceName ?? 'test', - devModeEnabled = devModeEnabled ?? true, _isSupported = deviceSupported, _isSupportedForProject = deviceSupportForProject; @@ -2799,6 +2838,7 @@ class FakeIOSDevice extends Fake implements IOSDevice { this.isConnected = false, this.platformType = PlatformType.ios, this.devModeEnabled = true, + this.isPaired = true, this.connectionInterface = DeviceConnectionInterface.wireless, }) : id = deviceId ?? 'xxx', name = deviceName ?? 'test', @@ -2813,6 +2853,7 @@ class FakeIOSDevice extends Fake implements IOSDevice { this.ephemeral = true, this.isConnected = true, this.devModeEnabled = true, + this.isPaired = true, this.platformType = PlatformType.ios, this.connectionInterface = DeviceConnectionInterface.wireless, }) : id = deviceId ?? 'xxx', @@ -2832,6 +2873,9 @@ class FakeIOSDevice extends Fake implements IOSDevice { @override final bool devModeEnabled; + @override + final bool isPaired; + @override String id;