• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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