device.dart 15 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
Devon Carew's avatar
Devon Carew committed
6
import 'dart:math' as math;
7

8
import 'android/android_device.dart';
9
import 'application_package.dart';
10
import 'base/context.dart';
11
import 'base/file_system.dart';
12
import 'base/utils.dart';
13
import 'build_info.dart';
14
import 'desktop.dart';
15 16
import 'fuchsia/fuchsia_device.dart';

Devon Carew's avatar
Devon Carew committed
17
import 'globals.dart';
18 19
import 'ios/devices.dart';
import 'ios/simulators.dart';
20 21
import 'linux/linux_device.dart';
import 'macos/macos_device.dart';
22
import 'tester/flutter_tester.dart';
23
import 'web/web_device.dart';
24
import 'windows/windows_device.dart';
25

26 27
DeviceManager get deviceManager => context[DeviceManager];

28 29
/// A class to get all available devices.
class DeviceManager {
30

31
  /// Constructing DeviceManagers is cheap; they only do expensive work if some
32
  /// of their methods are called.
33 34 35 36 37 38 39
  List<DeviceDiscovery> get deviceDiscoverers => _deviceDiscoverers;
  final List<DeviceDiscovery> _deviceDiscoverers = List<DeviceDiscovery>.unmodifiable(<DeviceDiscovery>[
    AndroidDevices(),
    IOSDevices(),
    IOSSimulators(),
    FuchsiaDevices(),
    FlutterTesterDevices(),
40
  ] + _conditionalDesktopDevices + _conditionalWebDevices);
41 42 43 44 45 46 47 48 49

  /// Only add desktop devices if the flag is enabled.
  static List<DeviceDiscovery> get _conditionalDesktopDevices {
    return flutterDesktopEnabled ? <DeviceDiscovery>[
      MacOSDevices(),
      LinuxDevices(),
      WindowsDevices(),
    ] : <DeviceDiscovery>[];
  }
50

51 52 53 54 55 56 57
  /// Only add web devices if the flag is enabled.
  static List<DeviceDiscovery> get _conditionalWebDevices {
    return flutterWebEnabled ? <DeviceDiscovery>[
      WebDevices(),
    ] : <DeviceDiscovery>[];
  }

58 59
  String _specifiedDeviceId;

60
  /// A user-specified device ID.
61 62 63 64 65
  String get specifiedDeviceId {
    if (_specifiedDeviceId == null || _specifiedDeviceId == 'all')
      return null;
    return _specifiedDeviceId;
  }
66

67 68 69 70 71
  set specifiedDeviceId(String id) {
    _specifiedDeviceId = id;
  }

  /// True when the user has specified a single specific device.
72
  bool get hasSpecifiedDeviceId => specifiedDeviceId != null;
73

74 75 76 77
  /// True when the user has specified all devices by setting
  /// specifiedDeviceId = 'all'.
  bool get hasSpecifiedAllDevices => _specifiedDeviceId == 'all';

78
  Stream<Device> getDevicesById(String deviceId) async* {
79
    final List<Device> devices = await getAllConnectedDevices().toList();
Devon Carew's avatar
Devon Carew committed
80
    deviceId = deviceId.toLowerCase();
81 82 83 84 85 86 87
    bool exactlyMatchesDeviceId(Device device) =>
        device.id.toLowerCase() == deviceId ||
        device.name.toLowerCase() == deviceId;
    bool startsWithDeviceId(Device device) =>
        device.id.toLowerCase().startsWith(deviceId) ||
        device.name.toLowerCase().startsWith(deviceId);

88 89
    final Device exactMatch = devices.firstWhere(
        exactlyMatchesDeviceId, orElse: () => null);
90 91 92 93
    if (exactMatch != null) {
      yield exactMatch;
      return;
    }
94

95
    // Match on a id or name starting with [deviceId].
96
    for (Device device in devices.where(startsWithDeviceId))
97
      yield device;
Devon Carew's avatar
Devon Carew committed
98 99
  }

100
  /// Return the list of connected devices, filtered by any user-specified device id.
101 102 103 104
  Stream<Device> getDevices() {
    return hasSpecifiedDeviceId
        ? getDevicesById(specifiedDeviceId)
        : getAllConnectedDevices();
105 106
  }

107
  Iterable<DeviceDiscovery> get _platformDiscoverers {
108
    return deviceDiscoverers.where((DeviceDiscovery discoverer) => discoverer.supportsPlatform);
109 110
  }

111
  /// Return the list of all connected devices.
112 113 114 115 116 117
  Stream<Device> getAllConnectedDevices() async* {
    for (DeviceDiscovery discoverer in _platformDiscoverers) {
      for (Device device in await discoverer.devices) {
        yield device;
      }
    }
118
  }
119 120 121 122 123 124 125 126 127 128 129 130 131 132

  /// Whether we're capable of listing any devices given the current environment configuration.
  bool get canListAnything {
    return _platformDiscoverers.any((DeviceDiscovery discoverer) => discoverer.canListAnything);
  }

  /// Get diagnostics about issues with any connected devices.
  Future<List<String>> getDeviceDiagnostics() async {
    final List<String> diagnostics = <String>[];
    for (DeviceDiscovery discoverer in _platformDiscoverers) {
      diagnostics.addAll(await discoverer.getDiagnostics());
    }
    return diagnostics;
  }
133 134 135 136 137
}

/// An abstract class to discover and enumerate a specific type of devices.
abstract class DeviceDiscovery {
  bool get supportsPlatform;
138 139 140 141 142

  /// Whether this device discovery is capable of listing any devices given the
  /// current environment configuration.
  bool get canListAnything;

143
  Future<List<Device>> get devices;
144 145 146

  /// Gets a list of diagnostic messages pertaining to issues with any connected
  /// devices (will be an empty list if there are no issues).
147
  Future<List<String>> getDiagnostics() => Future<List<String>>.value(<String>[]);
148 149
}

150 151 152 153 154
/// A [DeviceDiscovery] implementation that uses polling to discover device adds
/// and removals.
abstract class PollingDeviceDiscovery extends DeviceDiscovery {
  PollingDeviceDiscovery(this.name);

155 156
  static const Duration _pollingInterval = Duration(seconds: 4);
  static const Duration _pollingTimeout = Duration(seconds: 30);
157 158 159

  final String name;
  ItemListNotifier<Device> _items;
160
  Poller _poller;
161

162
  Future<List<Device>> pollingGetDevices();
163 164

  void startPolling() {
165
    if (_poller == null) {
166
      _items ??= ItemListNotifier<Device>();
167

168
      _poller = Poller(() async {
169 170 171 172
        try {
          final List<Device> devices = await pollingGetDevices().timeout(_pollingTimeout);
          _items.updateWithNewList(devices);
        } on TimeoutException {
173
          printTrace('Device poll timed out. Will retry.');
174
        }
175
      }, _pollingInterval);
176 177 178 179
    }
  }

  void stopPolling() {
180 181
    _poller?.cancel();
    _poller = null;
182 183
  }

184
  @override
185
  Future<List<Device>> get devices async {
186
    _items ??= ItemListNotifier<Device>.from(await pollingGetDevices());
187 188 189 190
    return _items.items;
  }

  Stream<Device> get onAdded {
191
    _items ??= ItemListNotifier<Device>();
192 193 194 195
    return _items.onAdded;
  }

  Stream<Device> get onRemoved {
196
    _items ??= ItemListNotifier<Device>();
197 198 199 200 201
    return _items.onRemoved;
  }

  void dispose() => stopPolling();

202
  @override
203 204 205
  String toString() => '$name device discovery';
}

206
abstract class Device {
207

208
  Device(this.id);
209

210
  final String id;
211

212 213
  String get name;

214 215
  bool get supportsStartPaused => true;

216
  /// Whether it is an emulated device running on localhost.
217
  Future<bool> get isLocalEmulator;
218

219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
  /// Whether the device is a simulator on a platform which supports hardware rendering.
  Future<bool> get supportsHardwareRendering async {
    assert(await isLocalEmulator);
    switch (await targetPlatform) {
      case TargetPlatform.android_arm:
      case TargetPlatform.android_arm64:
      case TargetPlatform.android_x64:
      case TargetPlatform.android_x86:
        return true;
      case TargetPlatform.ios:
      case TargetPlatform.darwin_x64:
      case TargetPlatform.linux_x64:
      case TargetPlatform.windows_x64:
      case TargetPlatform.fuchsia:
      default:
        return false;
    }
  }

238
  /// Check if a version of the given app is already installed
239
  Future<bool> isAppInstalled(ApplicationPackage app);
240

241
  /// Check if the latest build of the [app] is already installed.
242
  Future<bool> isLatestBuildInstalled(ApplicationPackage app);
243

244
  /// Install an app package on the current device
245
  Future<bool> installApp(ApplicationPackage app);
246

247
  /// Uninstall an app package from the current device
248
  Future<bool> uninstallApp(ApplicationPackage app);
249

250 251 252 253 254
  /// Check if the device is supported by Flutter
  bool isSupported();

  // String meant to be displayed to the user indicating if the device is
  // supported by Flutter, and, if not, why.
255
  String supportMessage() => isSupported() ? 'Supported' : 'Unsupported';
256

257 258
  /// The device's platform.
  Future<TargetPlatform> get targetPlatform;
259

260
  Future<String> get sdkNameAndVersion;
261

262 263 264
  /// Get a log reader for this device.
  /// If [app] is specified, this will return a log reader specific to that
  /// application. Otherwise, a global log reader will be returned.
265
  DeviceLogReader getLogReader({ ApplicationPackage app });
266

267 268 269
  /// Get the port forwarder for this device.
  DevicePortForwarder get portForwarder;

270 271
  /// Clear the device's logs.
  void clearLogs();
272

273 274 275
  /// Start an app package on the current device.
  ///
  /// [platformArgs] allows callers to pass platform-specific arguments to the
276
  /// start call. The build mode is not used by all platforms.
277 278 279 280 281
  ///
  /// If [usesTerminalUi] is true, Flutter Tools may attempt to prompt the
  /// user to resolve fixable issues such as selecting a signing certificate
  /// for iOS device deployment. Set to false if stdin cannot be read from while
  /// attempting to start the app.
Devon Carew's avatar
Devon Carew committed
282
  Future<LaunchResult> startApp(
283
    ApplicationPackage package, {
284 285
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
286
    DebuggingOptions debuggingOptions,
287
    Map<String, dynamic> platformArgs,
288 289 290
    bool prebuiltApplication = false,
    bool usesTerminalUi = true,
    bool ipv6 = false,
291
  });
292

293 294 295 296 297
  /// Whether this device implements support for hot reload.
  bool get supportsHotReload => true;

  /// Whether this device implements support for hot restart.
  bool get supportsHotRestart => true;
298

299 300 301
  /// Whether flutter applications running on this device can be terminated
  /// from the vmservice.
  bool get supportsStopApp => true;
302

303 304
  /// Whether the device supports taking screenshots of a running flutter
  /// application.
Devon Carew's avatar
Devon Carew committed
305 306
  bool get supportsScreenshot => false;

307 308 309
  /// Stop an app package on the current device.
  Future<bool> stopApp(ApplicationPackage app);

310
  Future<void> takeScreenshot(File outputFile) => Future<void>.error('unimplemented');
Devon Carew's avatar
Devon Carew committed
311

312
  @override
313 314
  int get hashCode => id.hashCode;

315
  @override
316 317 318 319 320 321 322 323
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! Device)
      return false;
    return id == other.id;
  }

324
  @override
325
  String toString() => name;
Devon Carew's avatar
Devon Carew committed
326

327
  static Stream<String> descriptions(List<Device> devices) async* {
328
    if (devices.isEmpty)
329
      return;
Devon Carew's avatar
Devon Carew committed
330

331
    // Extract device information
332
    final List<List<String>> table = <List<String>>[];
Devon Carew's avatar
Devon Carew committed
333 334
    for (Device device in devices) {
      String supportIndicator = device.isSupported() ? '' : ' (unsupported)';
335 336 337
      final TargetPlatform targetPlatform = await device.targetPlatform;
      if (await device.isLocalEmulator) {
        final String type = targetPlatform == TargetPlatform.ios ? 'simulator' : 'emulator';
338 339
        supportIndicator += ' ($type)';
      }
340 341 342
      table.add(<String>[
        device.name,
        device.id,
343 344
        '${getNameForTargetPlatform(targetPlatform)}',
        '${await device.sdkNameAndVersion}$supportIndicator',
345 346 347 348
      ]);
    }

    // Calculate column widths
349
    final List<int> indices = List<int>.generate(table[0].length - 1, (int i) => i);
350
    List<int> widths = indices.map<int>((int i) => 0).toList();
351
    for (List<String> row in table) {
352
      widths = indices.map<int>((int i) => math.max(widths[i], row[i].length)).toList();
353 354 355
    }

    // Join columns into lines of text
356
    for (List<String> row in table) {
357
      yield indices.map<String>((int i) => row[i].padRight(widths[i])).join(' • ') + ' • ${row.last}';
358
    }
359 360
  }

361
  static Future<void> printDevices(List<Device> devices) async {
362
    await descriptions(devices).forEach(printStatus);
Devon Carew's avatar
Devon Carew committed
363
  }
364 365
}

Devon Carew's avatar
Devon Carew committed
366
class DebuggingOptions {
367 368
  DebuggingOptions.enabled(
    this.buildInfo, {
369
    this.startPaused = false,
370
    this.disableServiceAuthCodes = false,
371 372 373
    this.enableSoftwareRendering = false,
    this.skiaDeterministicRendering = false,
    this.traceSkia = false,
374
    this.traceSystrace = false,
375
    this.dumpSkpOnShaderCompilation = false,
376
    this.useTestFonts = false,
377
    this.verboseSystemLogs = false,
Devon Carew's avatar
Devon Carew committed
378 379 380
    this.observatoryPort,
   }) : debuggingEnabled = true;

381 382 383 384
  DebuggingOptions.disabled(this.buildInfo)
    : debuggingEnabled = false,
      useTestFonts = false,
      startPaused = false,
385
      disableServiceAuthCodes = false,
386 387 388 389
      enableSoftwareRendering = false,
      skiaDeterministicRendering = false,
      traceSkia = false,
      traceSystrace = false,
390
      dumpSkpOnShaderCompilation = false,
391
      verboseSystemLogs = false,
392
      observatoryPort = null;
Devon Carew's avatar
Devon Carew committed
393 394 395

  final bool debuggingEnabled;

396
  final BuildInfo buildInfo;
Devon Carew's avatar
Devon Carew committed
397
  final bool startPaused;
398
  final bool disableServiceAuthCodes;
399
  final bool enableSoftwareRendering;
400
  final bool skiaDeterministicRendering;
401
  final bool traceSkia;
402
  final bool traceSystrace;
403
  final bool dumpSkpOnShaderCompilation;
404
  final bool useTestFonts;
405
  final bool verboseSystemLogs;
Devon Carew's avatar
Devon Carew committed
406 407 408 409 410 411
  final int observatoryPort;

  bool get hasObservatoryPort => observatoryPort != null;
}

class LaunchResult {
412
  LaunchResult.succeeded({ this.observatoryUri }) : started = true;
413 414 415
  LaunchResult.failed()
    : started = false,
      observatoryUri = null;
Devon Carew's avatar
Devon Carew committed
416

417
  bool get hasObservatory => observatoryUri != null;
Devon Carew's avatar
Devon Carew committed
418 419

  final bool started;
420
  final Uri observatoryUri;
Devon Carew's avatar
Devon Carew committed
421 422 423

  @override
  String toString() {
424
    final StringBuffer buf = StringBuffer('started=$started');
425 426
    if (observatoryUri != null)
      buf.write(', observatory=$observatoryUri');
Devon Carew's avatar
Devon Carew committed
427 428 429 430
    return buf.toString();
  }
}

431
class ForwardedPort {
432 433
  ForwardedPort(this.hostPort, this.devicePort) : context = null;
  ForwardedPort.withContext(this.hostPort, this.devicePort, this.context);
434 435 436

  final int hostPort;
  final int devicePort;
437
  final dynamic context;
438

439
  @override
440 441 442 443 444 445 446 447 448 449
  String toString() => 'ForwardedPort HOST:$hostPort to DEVICE:$devicePort';
}

/// Forward ports from the host machine to the device.
abstract class DevicePortForwarder {
  /// Returns a Future that completes with the current list of forwarded
  /// ports for this device.
  List<ForwardedPort> get forwardedPorts;

  /// Forward [hostPort] on the host to [devicePort] on the device.
450
  /// If [hostPort] is null or zero, will auto select a host port.
451
  /// Returns a Future that completes with the host port.
452
  Future<int> forward(int devicePort, { int hostPort });
453 454

  /// Stops forwarding [forwardedPort].
455
  Future<void> unforward(ForwardedPort forwardedPort);
456 457
}

Devon Carew's avatar
Devon Carew committed
458
/// Read the log for a particular device.
Devon Carew's avatar
Devon Carew committed
459 460 461
abstract class DeviceLogReader {
  String get name;

Devon Carew's avatar
Devon Carew committed
462 463
  /// A broadcast stream where each element in the string is a line of log output.
  Stream<String> get logLines;
Devon Carew's avatar
Devon Carew committed
464

465
  @override
Devon Carew's avatar
Devon Carew committed
466
  String toString() => name;
467

468
  /// Process ID of the app on the device.
469
  int appPid;
Devon Carew's avatar
Devon Carew committed
470
}
471 472 473

/// Describes an app running on the device.
class DiscoveredApp {
474
  DiscoveredApp(this.id, this.observatoryPort);
475 476 477
  final String id;
  final int observatoryPort;
}
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497

// An empty device log reader
class NoOpDeviceLogReader implements DeviceLogReader {
  NoOpDeviceLogReader(this.name);

  @override
  final String name;

  @override
  int appPid;

  @override
  Stream<String> get logLines => const Stream<String>.empty();
}

// A portforwarder which does not support forwarding ports.
class NoOpDevicePortForwarder implements DevicePortForwarder {
  const NoOpDevicePortForwarder();

  @override
498
  Future<int> forward(int devicePort, { int hostPort }) async => devicePort;
499 500 501 502 503

  @override
  List<ForwardedPort> get forwardedPorts => <ForwardedPort>[];

  @override
504
  Future<void> unforward(ForwardedPort forwardedPort) async { }
505
}