1// Copyright 2015 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6import 'dart:math' as math; 7 8import 'package:meta/meta.dart'; 9 10import 'android/android_device.dart'; 11import 'application_package.dart'; 12import 'artifacts.dart'; 13import 'base/context.dart'; 14import 'base/file_system.dart'; 15import 'base/utils.dart'; 16import 'build_info.dart'; 17import 'fuchsia/fuchsia_device.dart'; 18import 'globals.dart'; 19import 'ios/devices.dart'; 20import 'ios/simulators.dart'; 21import 'linux/linux_device.dart'; 22import 'macos/macos_device.dart'; 23import 'project.dart'; 24import 'tester/flutter_tester.dart'; 25import 'web/web_device.dart'; 26import 'windows/windows_device.dart'; 27 28DeviceManager get deviceManager => context.get<DeviceManager>(); 29 30/// A description of the kind of workflow the device supports. 31class Category { 32 const Category._(this.value); 33 34 static const Category web = Category._('web'); 35 static const Category desktop = Category._('desktop'); 36 static const Category mobile = Category._('mobile'); 37 38 final String value; 39 40 @override 41 String toString() => value; 42} 43 44/// The platform sub-folder that a device type supports. 45class PlatformType { 46 const PlatformType._(this.value); 47 48 static const PlatformType web = PlatformType._('web'); 49 static const PlatformType android = PlatformType._('android'); 50 static const PlatformType ios = PlatformType._('ios'); 51 static const PlatformType linux = PlatformType._('linux'); 52 static const PlatformType macos = PlatformType._('macos'); 53 static const PlatformType windows = PlatformType._('windows'); 54 static const PlatformType fuchsia = PlatformType._('fuchsia'); 55 56 final String value; 57 58 @override 59 String toString() => value; 60} 61 62/// A class to get all available devices. 63class DeviceManager { 64 65 /// Constructing DeviceManagers is cheap; they only do expensive work if some 66 /// of their methods are called. 67 List<DeviceDiscovery> get deviceDiscoverers => _deviceDiscoverers; 68 final List<DeviceDiscovery> _deviceDiscoverers = List<DeviceDiscovery>.unmodifiable(<DeviceDiscovery>[ 69 AndroidDevices(), 70 IOSDevices(), 71 IOSSimulators(), 72 FuchsiaDevices(), 73 FlutterTesterDevices(), 74 MacOSDevices(), 75 LinuxDevices(), 76 WindowsDevices(), 77 WebDevices(), 78 ]); 79 80 String _specifiedDeviceId; 81 82 /// A user-specified device ID. 83 String get specifiedDeviceId { 84 if (_specifiedDeviceId == null || _specifiedDeviceId == 'all') 85 return null; 86 return _specifiedDeviceId; 87 } 88 89 set specifiedDeviceId(String id) { 90 _specifiedDeviceId = id; 91 } 92 93 /// True when the user has specified a single specific device. 94 bool get hasSpecifiedDeviceId => specifiedDeviceId != null; 95 96 /// True when the user has specified all devices by setting 97 /// specifiedDeviceId = 'all'. 98 bool get hasSpecifiedAllDevices => _specifiedDeviceId == 'all'; 99 100 Stream<Device> getDevicesById(String deviceId) async* { 101 final List<Device> devices = await getAllConnectedDevices().toList(); 102 deviceId = deviceId.toLowerCase(); 103 bool exactlyMatchesDeviceId(Device device) => 104 device.id.toLowerCase() == deviceId || 105 device.name.toLowerCase() == deviceId; 106 bool startsWithDeviceId(Device device) => 107 device.id.toLowerCase().startsWith(deviceId) || 108 device.name.toLowerCase().startsWith(deviceId); 109 110 final Device exactMatch = devices.firstWhere( 111 exactlyMatchesDeviceId, orElse: () => null); 112 if (exactMatch != null) { 113 yield exactMatch; 114 return; 115 } 116 117 // Match on a id or name starting with [deviceId]. 118 for (Device device in devices.where(startsWithDeviceId)) 119 yield device; 120 } 121 122 /// Return the list of connected devices, filtered by any user-specified device id. 123 Stream<Device> getDevices() { 124 return hasSpecifiedDeviceId 125 ? getDevicesById(specifiedDeviceId) 126 : getAllConnectedDevices(); 127 } 128 129 Iterable<DeviceDiscovery> get _platformDiscoverers { 130 return deviceDiscoverers.where((DeviceDiscovery discoverer) => discoverer.supportsPlatform); 131 } 132 133 /// Return the list of all connected devices. 134 Stream<Device> getAllConnectedDevices() async* { 135 for (DeviceDiscovery discoverer in _platformDiscoverers) { 136 for (Device device in await discoverer.devices) { 137 yield device; 138 } 139 } 140 } 141 142 /// Whether we're capable of listing any devices given the current environment configuration. 143 bool get canListAnything { 144 return _platformDiscoverers.any((DeviceDiscovery discoverer) => discoverer.canListAnything); 145 } 146 147 /// Get diagnostics about issues with any connected devices. 148 Future<List<String>> getDeviceDiagnostics() async { 149 return <String>[ 150 for (DeviceDiscovery discoverer in _platformDiscoverers) 151 ...await discoverer.getDiagnostics(), 152 ]; 153 } 154 155 /// Find and return a list of devices based on the current project and environment. 156 /// 157 /// Returns a list of deviecs specified by the user. 158 /// 159 /// * If the user specified '-d all', then return all connected devices which 160 /// support the current project, except for fuchsia and web. 161 /// 162 /// * If the user specified a device id, then do nothing as the list is already 163 /// filtered by [getDevices]. 164 /// 165 /// * If the user did not specify a device id and there is more than one 166 /// device connected, then filter out unsupported devices and prioritize 167 /// ephemeral devices. 168 Future<List<Device>> findTargetDevices(FlutterProject flutterProject) async { 169 List<Device> devices = await getDevices().toList(); 170 171 // Always remove web and fuchsia devices from `--all`. This setting 172 // currently requires devices to share a frontend_server and resident 173 // runnner instance. Both web and fuchsia require differently configured 174 // compilers, and web requires an entirely different resident runner. 175 if (hasSpecifiedAllDevices) { 176 devices = <Device>[ 177 for (Device device in devices) 178 if (await device.targetPlatform != TargetPlatform.fuchsia && 179 await device.targetPlatform != TargetPlatform.web_javascript) 180 device 181 ]; 182 } 183 184 // If there is no specified device, the remove all devices which are not 185 // supported by the current application. For example, if there was no 186 // 'android' folder then don't attempt to launch with an Android device. 187 if (devices.length > 1 && !hasSpecifiedDeviceId) { 188 devices = <Device>[ 189 for (Device device in devices) 190 if (isDeviceSupportedForProject(device, flutterProject)) 191 device 192 ]; 193 } 194 195 // If there are still multiple devices and the user did not specify to run 196 // all, then attempt to prioritize ephemeral devices. For example, if the 197 // use only typed 'flutter run' and both an Android device and desktop 198 // device are availible, choose the Android device. 199 if (devices.length > 1 && !hasSpecifiedAllDevices) { 200 // Note: ephemeral is nullable for device types where this is not well 201 // defined. 202 if (devices.any((Device device) => device.ephemeral == true)) { 203 devices = devices 204 .where((Device device) => device.ephemeral == true) 205 .toList(); 206 } 207 } 208 return devices; 209 } 210 211 /// Returns whether the device is supported for the project. 212 /// 213 /// This exists to allow the check to be overriden for google3 clients. 214 bool isDeviceSupportedForProject(Device device, FlutterProject flutterProject) { 215 return device.isSupportedForProject(flutterProject); 216 } 217} 218 219/// An abstract class to discover and enumerate a specific type of devices. 220abstract class DeviceDiscovery { 221 bool get supportsPlatform; 222 223 /// Whether this device discovery is capable of listing any devices given the 224 /// current environment configuration. 225 bool get canListAnything; 226 227 Future<List<Device>> get devices; 228 229 /// Gets a list of diagnostic messages pertaining to issues with any connected 230 /// devices (will be an empty list if there are no issues). 231 Future<List<String>> getDiagnostics() => Future<List<String>>.value(<String>[]); 232} 233 234/// A [DeviceDiscovery] implementation that uses polling to discover device adds 235/// and removals. 236abstract class PollingDeviceDiscovery extends DeviceDiscovery { 237 PollingDeviceDiscovery(this.name); 238 239 static const Duration _pollingInterval = Duration(seconds: 4); 240 static const Duration _pollingTimeout = Duration(seconds: 30); 241 242 final String name; 243 ItemListNotifier<Device> _items; 244 Poller _poller; 245 246 Future<List<Device>> pollingGetDevices(); 247 248 void startPolling() { 249 if (_poller == null) { 250 _items ??= ItemListNotifier<Device>(); 251 252 _poller = Poller(() async { 253 try { 254 final List<Device> devices = await pollingGetDevices().timeout(_pollingTimeout); 255 _items.updateWithNewList(devices); 256 } on TimeoutException { 257 printTrace('Device poll timed out. Will retry.'); 258 } 259 }, _pollingInterval); 260 } 261 } 262 263 void stopPolling() { 264 _poller?.cancel(); 265 _poller = null; 266 } 267 268 @override 269 Future<List<Device>> get devices async { 270 _items ??= ItemListNotifier<Device>.from(await pollingGetDevices()); 271 return _items.items; 272 } 273 274 Stream<Device> get onAdded { 275 _items ??= ItemListNotifier<Device>(); 276 return _items.onAdded; 277 } 278 279 Stream<Device> get onRemoved { 280 _items ??= ItemListNotifier<Device>(); 281 return _items.onRemoved; 282 } 283 284 void dispose() => stopPolling(); 285 286 @override 287 String toString() => '$name device discovery'; 288} 289 290abstract class Device { 291 292 Device(this.id, {@required this.category, @required this.platformType, @required this.ephemeral}); 293 294 final String id; 295 296 /// The [Category] for this device type. 297 final Category category; 298 299 /// The [PlatformType] for this device. 300 final PlatformType platformType; 301 302 /// Whether this is an ephemeral device. 303 final bool ephemeral; 304 305 String get name; 306 307 bool get supportsStartPaused => true; 308 309 /// Whether it is an emulated device running on localhost. 310 Future<bool> get isLocalEmulator; 311 312 /// The unique identifier for the emulator that corresponds to this device, or 313 /// null if it is not an emulator. 314 /// 315 /// The ID returned matches that in the output of `flutter emulators`. Fetching 316 /// this name may require connecting to the device and if an error occurs null 317 /// will be returned. 318 Future<String> get emulatorId; 319 320 /// Whether the device is a simulator on a platform which supports hardware rendering. 321 Future<bool> get supportsHardwareRendering async { 322 assert(await isLocalEmulator); 323 switch (await targetPlatform) { 324 case TargetPlatform.android_arm: 325 case TargetPlatform.android_arm64: 326 case TargetPlatform.android_x64: 327 case TargetPlatform.android_x86: 328 return true; 329 case TargetPlatform.ios: 330 case TargetPlatform.darwin_x64: 331 case TargetPlatform.linux_x64: 332 case TargetPlatform.windows_x64: 333 case TargetPlatform.fuchsia: 334 default: 335 return false; 336 } 337 } 338 339 /// Whether the device is supported for the current project directory. 340 bool isSupportedForProject(FlutterProject flutterProject); 341 342 /// Check if a version of the given app is already installed 343 Future<bool> isAppInstalled(ApplicationPackage app); 344 345 /// Check if the latest build of the [app] is already installed. 346 Future<bool> isLatestBuildInstalled(ApplicationPackage app); 347 348 /// Install an app package on the current device 349 Future<bool> installApp(ApplicationPackage app); 350 351 /// Uninstall an app package from the current device 352 Future<bool> uninstallApp(ApplicationPackage app); 353 354 /// Check if the device is supported by Flutter 355 bool isSupported(); 356 357 // String meant to be displayed to the user indicating if the device is 358 // supported by Flutter, and, if not, why. 359 String supportMessage() => isSupported() ? 'Supported' : 'Unsupported'; 360 361 /// The device's platform. 362 Future<TargetPlatform> get targetPlatform; 363 364 Future<String> get sdkNameAndVersion; 365 366 /// Get a log reader for this device. 367 /// If [app] is specified, this will return a log reader specific to that 368 /// application. Otherwise, a global log reader will be returned. 369 DeviceLogReader getLogReader({ ApplicationPackage app }); 370 371 /// Get the port forwarder for this device. 372 DevicePortForwarder get portForwarder; 373 374 /// Clear the device's logs. 375 void clearLogs(); 376 377 /// Optional device-specific artifact overrides. 378 OverrideArtifacts get artifactOverrides => null; 379 380 /// Start an app package on the current device. 381 /// 382 /// [platformArgs] allows callers to pass platform-specific arguments to the 383 /// start call. The build mode is not used by all platforms. 384 /// 385 /// If [usesTerminalUi] is true, Flutter Tools may attempt to prompt the 386 /// user to resolve fixable issues such as selecting a signing certificate 387 /// for iOS device deployment. Set to false if stdin cannot be read from while 388 /// attempting to start the app. 389 Future<LaunchResult> startApp( 390 ApplicationPackage package, { 391 String mainPath, 392 String route, 393 DebuggingOptions debuggingOptions, 394 Map<String, dynamic> platformArgs, 395 bool prebuiltApplication = false, 396 bool ipv6 = false, 397 bool usesTerminalUi = true, 398 }); 399 400 /// Whether this device implements support for hot reload. 401 bool get supportsHotReload => true; 402 403 /// Whether this device implements support for hot restart. 404 bool get supportsHotRestart => true; 405 406 /// Whether flutter applications running on this device can be terminated 407 /// from the vmservice. 408 bool get supportsFlutterExit => true; 409 410 /// Whether the device supports taking screenshots of a running flutter 411 /// application. 412 bool get supportsScreenshot => false; 413 414 /// Stop an app package on the current device. 415 Future<bool> stopApp(ApplicationPackage app); 416 417 Future<void> takeScreenshot(File outputFile) => Future<void>.error('unimplemented'); 418 419 @override 420 int get hashCode => id.hashCode; 421 422 @override 423 bool operator ==(dynamic other) { 424 if (identical(this, other)) 425 return true; 426 if (other is! Device) 427 return false; 428 return id == other.id; 429 } 430 431 @override 432 String toString() => name; 433 434 static Stream<String> descriptions(List<Device> devices) async* { 435 if (devices.isEmpty) 436 return; 437 438 // Extract device information 439 final List<List<String>> table = <List<String>>[]; 440 for (Device device in devices) { 441 String supportIndicator = device.isSupported() ? '' : ' (unsupported)'; 442 final TargetPlatform targetPlatform = await device.targetPlatform; 443 if (await device.isLocalEmulator) { 444 final String type = targetPlatform == TargetPlatform.ios ? 'simulator' : 'emulator'; 445 supportIndicator += ' ($type)'; 446 } 447 table.add(<String>[ 448 device.name, 449 device.id, 450 '${getNameForTargetPlatform(targetPlatform)}', 451 '${await device.sdkNameAndVersion}$supportIndicator', 452 ]); 453 } 454 455 // Calculate column widths 456 final List<int> indices = List<int>.generate(table[0].length - 1, (int i) => i); 457 List<int> widths = indices.map<int>((int i) => 0).toList(); 458 for (List<String> row in table) { 459 widths = indices.map<int>((int i) => math.max(widths[i], row[i].length)).toList(); 460 } 461 462 // Join columns into lines of text 463 for (List<String> row in table) { 464 yield indices.map<String>((int i) => row[i].padRight(widths[i])).join(' • ') + ' • ${row.last}'; 465 } 466 } 467 468 static Future<void> printDevices(List<Device> devices) async { 469 await descriptions(devices).forEach(printStatus); 470 } 471} 472 473class DebuggingOptions { 474 DebuggingOptions.enabled( 475 this.buildInfo, { 476 this.startPaused = false, 477 this.disableServiceAuthCodes = false, 478 this.dartFlags = '', 479 this.enableSoftwareRendering = false, 480 this.skiaDeterministicRendering = false, 481 this.traceSkia = false, 482 this.traceSystrace = false, 483 this.dumpSkpOnShaderCompilation = false, 484 this.useTestFonts = false, 485 this.verboseSystemLogs = false, 486 this.observatoryPort, 487 }) : debuggingEnabled = true; 488 489 DebuggingOptions.disabled(this.buildInfo) 490 : debuggingEnabled = false, 491 useTestFonts = false, 492 startPaused = false, 493 dartFlags = '', 494 disableServiceAuthCodes = false, 495 enableSoftwareRendering = false, 496 skiaDeterministicRendering = false, 497 traceSkia = false, 498 traceSystrace = false, 499 dumpSkpOnShaderCompilation = false, 500 verboseSystemLogs = false, 501 observatoryPort = null; 502 503 final bool debuggingEnabled; 504 505 final BuildInfo buildInfo; 506 final bool startPaused; 507 final String dartFlags; 508 final bool disableServiceAuthCodes; 509 final bool enableSoftwareRendering; 510 final bool skiaDeterministicRendering; 511 final bool traceSkia; 512 final bool traceSystrace; 513 final bool dumpSkpOnShaderCompilation; 514 final bool useTestFonts; 515 final bool verboseSystemLogs; 516 final int observatoryPort; 517 518 bool get hasObservatoryPort => observatoryPort != null; 519} 520 521class LaunchResult { 522 LaunchResult.succeeded({ this.observatoryUri }) : started = true; 523 LaunchResult.failed() 524 : started = false, 525 observatoryUri = null; 526 527 bool get hasObservatory => observatoryUri != null; 528 529 final bool started; 530 final Uri observatoryUri; 531 532 @override 533 String toString() { 534 final StringBuffer buf = StringBuffer('started=$started'); 535 if (observatoryUri != null) 536 buf.write(', observatory=$observatoryUri'); 537 return buf.toString(); 538 } 539} 540 541class ForwardedPort { 542 ForwardedPort(this.hostPort, this.devicePort) : context = null; 543 ForwardedPort.withContext(this.hostPort, this.devicePort, this.context); 544 545 final int hostPort; 546 final int devicePort; 547 final dynamic context; 548 549 @override 550 String toString() => 'ForwardedPort HOST:$hostPort to DEVICE:$devicePort'; 551} 552 553/// Forward ports from the host machine to the device. 554abstract class DevicePortForwarder { 555 /// Returns a Future that completes with the current list of forwarded 556 /// ports for this device. 557 List<ForwardedPort> get forwardedPorts; 558 559 /// Forward [hostPort] on the host to [devicePort] on the device. 560 /// If [hostPort] is null or zero, will auto select a host port. 561 /// Returns a Future that completes with the host port. 562 Future<int> forward(int devicePort, { int hostPort }); 563 564 /// Stops forwarding [forwardedPort]. 565 Future<void> unforward(ForwardedPort forwardedPort); 566} 567 568/// Read the log for a particular device. 569abstract class DeviceLogReader { 570 String get name; 571 572 /// A broadcast stream where each element in the string is a line of log output. 573 Stream<String> get logLines; 574 575 @override 576 String toString() => name; 577 578 /// Process ID of the app on the device. 579 int appPid; 580} 581 582/// Describes an app running on the device. 583class DiscoveredApp { 584 DiscoveredApp(this.id, this.observatoryPort); 585 final String id; 586 final int observatoryPort; 587} 588 589// An empty device log reader 590class NoOpDeviceLogReader implements DeviceLogReader { 591 NoOpDeviceLogReader(this.name); 592 593 @override 594 final String name; 595 596 @override 597 int appPid; 598 599 @override 600 Stream<String> get logLines => const Stream<String>.empty(); 601} 602 603// A portforwarder which does not support forwarding ports. 604class NoOpDevicePortForwarder implements DevicePortForwarder { 605 const NoOpDevicePortForwarder(); 606 607 @override 608 Future<int> forward(int devicePort, { int hostPort }) async => devicePort; 609 610 @override 611 List<ForwardedPort> get forwardedPorts => <ForwardedPort>[]; 612 613 @override 614 Future<void> unforward(ForwardedPort forwardedPort) async { } 615} 616