Add --device-timeout flag to device-related commands (#64834)

diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index 7272767..0cde25a 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -102,6 +102,7 @@
       );
     usesTrackWidgetCreation(verboseHelp: verboseHelp);
     addDdsOptions(verboseHelp: verboseHelp);
+    usesDeviceTimeoutOption();
     hotRunnerFactory ??= HotRunnerFactory();
   }
 
diff --git a/packages/flutter_tools/lib/src/commands/devices.dart b/packages/flutter_tools/lib/src/commands/devices.dart
index 53c9eea..b513539 100644
--- a/packages/flutter_tools/lib/src/commands/devices.dart
+++ b/packages/flutter_tools/lib/src/commands/devices.dart
@@ -22,8 +22,9 @@
       'timeout',
       abbr: 't',
       defaultsTo: null,
-      help: 'Time in seconds to wait for devices to attach. Longer timeouts may be necessary for networked devices.'
+      help: '(deprecated) Use --device-timeout instead',
     );
+    usesDeviceTimeoutOption();
   }
 
   @override
@@ -32,20 +33,25 @@
   @override
   final String description = 'List all connected devices.';
 
-  Duration get timeout {
-    if (argResults['timeout'] == null) {
-      return null;
-    }
-    if (_timeout == null) {
+  @override
+  Duration get deviceDiscoveryTimeout {
+    if (argResults['timeout'] != null) {
       final int timeoutSeconds = int.tryParse(stringArg('timeout'));
       if (timeoutSeconds == null) {
         throwToolExit( 'Could not parse -t/--timeout argument. It must be an integer.');
       }
-      _timeout = Duration(seconds: timeoutSeconds);
+      return Duration(seconds: timeoutSeconds);
     }
-    return _timeout;
+    return super.deviceDiscoveryTimeout;
   }
-  Duration _timeout;
+
+  @override
+  Future<void> validateCommand() {
+    if (argResults['timeout'] != null) {
+      globals.printError('--timeout has been deprecated, use --${FlutterOptions.kDeviceTimeout} instead');
+    }
+    return super.validateCommand();
+  }
 
   @override
   Future<FlutterCommandResult> runCommand() async {
@@ -56,7 +62,7 @@
         exitCode: 1);
     }
 
-    final List<Device> devices = await globals.deviceManager.refreshAllConnectedDevices(timeout: timeout);
+    final List<Device> devices = await globals.deviceManager.refreshAllConnectedDevices(timeout: deviceDiscoveryTimeout);
 
     if (boolArg('machine')) {
       await printDevicesAsJson(devices);
@@ -68,8 +74,8 @@
         status.writeln('Run "flutter emulators" to list and start any available device emulators.');
         status.writeln();
         status.write('If you expected your device to be detected, please run "flutter doctor" to diagnose potential issues. ');
-        if (timeout == null) {
-          status.write('You may also try increasing the time to wait for connected devices with the --timeout flag. ');
+        if (deviceDiscoveryTimeout == null) {
+          status.write('You may also try increasing the time to wait for connected devices with the --${FlutterOptions.kDeviceTimeout} flag. ');
         }
         status.write('Visit https://flutter.dev/setup/ for troubleshooting tips.');
 
diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart
index 0980e68..1f9772e 100644
--- a/packages/flutter_tools/lib/src/commands/drive.dart
+++ b/packages/flutter_tools/lib/src/commands/drive.dart
@@ -146,7 +146,7 @@
   @override
   Future<void> validateCommand() async {
     if (userIdentifier != null) {
-      final Device device = await findTargetDevice();
+      final Device device = await findTargetDevice(timeout: deviceDiscoveryTimeout);
       if (device is! AndroidDevice) {
         throwToolExit('--${FlutterOptions.kDeviceUser} is only supported for Android');
       }
@@ -161,7 +161,7 @@
       throwToolExit(null);
     }
 
-    _device = await findTargetDevice();
+    _device = await findTargetDevice(timeout: deviceDiscoveryTimeout);
     if (device == null) {
       throwToolExit(null);
     }
@@ -388,9 +388,9 @@
   }
 }
 
-Future<Device> findTargetDevice() async {
+Future<Device> findTargetDevice({ @required Duration timeout }) async {
   final DeviceManager deviceManager = globals.deviceManager;
-  final List<Device> devices = await deviceManager.findTargetDevices(FlutterProject.current());
+  final List<Device> devices = await deviceManager.findTargetDevices(FlutterProject.current(), timeout: timeout);
 
   if (deviceManager.hasSpecifiedDeviceId) {
     if (devices.isEmpty) {
diff --git a/packages/flutter_tools/lib/src/commands/install.dart b/packages/flutter_tools/lib/src/commands/install.dart
index 8228cbf..62fd7cd 100644
--- a/packages/flutter_tools/lib/src/commands/install.dart
+++ b/packages/flutter_tools/lib/src/commands/install.dart
@@ -16,6 +16,7 @@
   InstallCommand() {
     requiresPubspecYaml();
     usesDeviceUserOption();
+    usesDeviceTimeoutOption();
     argParser.addFlag('uninstall-only',
       negatable: true,
       defaultsTo: false,
diff --git a/packages/flutter_tools/lib/src/commands/logs.dart b/packages/flutter_tools/lib/src/commands/logs.dart
index 5b9b9cb..5305e14 100644
--- a/packages/flutter_tools/lib/src/commands/logs.dart
+++ b/packages/flutter_tools/lib/src/commands/logs.dart
@@ -18,6 +18,7 @@
       abbr: 'c',
       help: 'Clear log history before reading from logs.',
     );
+    usesDeviceTimeoutOption();
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index fd5b739..b70a1a7 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -75,6 +75,7 @@
     usesTrackWidgetCreation(verboseHelp: verboseHelp);
     addNullSafetyModeOptions(hide: !verboseHelp);
     usesDeviceUserOption();
+    usesDeviceTimeoutOption();
   }
 
   bool get traceStartup => boolArg('trace-startup');
diff --git a/packages/flutter_tools/lib/src/commands/screenshot.dart b/packages/flutter_tools/lib/src/commands/screenshot.dart
index 3c32de7..566affd 100644
--- a/packages/flutter_tools/lib/src/commands/screenshot.dart
+++ b/packages/flutter_tools/lib/src/commands/screenshot.dart
@@ -51,6 +51,7 @@
       },
       defaultsTo: _kDeviceType,
     );
+    usesDeviceTimeoutOption();
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 2a16f8e..84d0a05 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -209,7 +209,12 @@
   /// * If the user did not specify a device id and there is more than one
   /// device connected, then filter out unsupported devices and prioritize
   /// ephemeral devices.
-  Future<List<Device>> findTargetDevices(FlutterProject flutterProject) async {
+  Future<List<Device>> findTargetDevices(FlutterProject flutterProject, { Duration timeout }) async {
+    if (timeout != null) {
+      // Reset the cache with the specified timeout.
+      await refreshAllConnectedDevices(timeout: timeout);
+    }
+
     List<Device> devices = await getDevices();
 
     // Always remove web and fuchsia devices from `--all`. This setting
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index e42548b..8fa2413 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -113,6 +113,7 @@
   static const String kPerformanceMeasurementFile = 'performance-measurement-file';
   static const String kNullSafety = 'sound-null-safety';
   static const String kDeviceUser = 'device-user';
+  static const String kDeviceTimeout = 'device-timeout';
   static const String kAnalyzeSize = 'analyze-size';
   static const String kNullAssertions = 'null-assertions';
 }
@@ -404,6 +405,28 @@
       valueHelp: '10');
   }
 
+  void usesDeviceTimeoutOption() {
+    argParser.addOption(
+      FlutterOptions.kDeviceTimeout,
+      help: 'Time in seconds to wait for devices to attach. Longer timeouts may be necessary for networked devices.',
+      valueHelp: '10'
+    );
+  }
+
+  Duration get deviceDiscoveryTimeout {
+    if (_deviceDiscoveryTimeout == null
+        && argResults.options.contains(FlutterOptions.kDeviceTimeout)
+        && argResults.wasParsed(FlutterOptions.kDeviceTimeout)) {
+      final int timeoutSeconds = int.tryParse(stringArg(FlutterOptions.kDeviceTimeout));
+      if (timeoutSeconds == null) {
+        throwToolExit( 'Could not parse --${FlutterOptions.kDeviceTimeout} argument. It must be an integer.');
+      }
+      _deviceDiscoveryTimeout = Duration(seconds: timeoutSeconds);
+    }
+    return _deviceDiscoveryTimeout;
+  }
+  Duration _deviceDiscoveryTimeout;
+
   void addBuildModeFlags({ bool defaultToRelease = true, bool verboseHelp = false, bool excludeDebug = false }) {
     // A release build must be the default if a debug build is not possible.
     assert(defaultToRelease || !excludeDebug);
@@ -970,7 +993,7 @@
       return null;
     }
     final DeviceManager deviceManager = globals.deviceManager;
-    List<Device> devices = await deviceManager.findTargetDevices(FlutterProject.current());
+    List<Device> devices = await deviceManager.findTargetDevices(FlutterProject.current(), timeout: deviceDiscoveryTimeout);
 
     if (devices.isEmpty && deviceManager.hasSpecifiedDeviceId) {
       globals.printStatus(userMessages.flutterNoMatchingDevice(deviceManager.specifiedDeviceId));