device.dart 13.3 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';
Devon Carew's avatar
Devon Carew committed
14
import 'globals.dart';
15 16
import 'ios/devices.dart';
import 'ios/simulators.dart';
17
import 'tester/flutter_tester.dart';
18

19 20
DeviceManager get deviceManager => context[DeviceManager];

21 22
/// A class to get all available devices.
class DeviceManager {
23
  /// Constructing DeviceManagers is cheap; they only do expensive work if some
24
  /// of their methods are called.
25
  DeviceManager() {
26
    // Register the known discoverers.
27 28 29
    _deviceDiscoverers.add(new AndroidDevices());
    _deviceDiscoverers.add(new IOSDevices());
    _deviceDiscoverers.add(new IOSSimulators());
30
    _deviceDiscoverers.add(new FlutterTesterDevices());
31 32
  }

33
  final List<DeviceDiscovery> _deviceDiscoverers = <DeviceDiscovery>[];
34

35 36
  String _specifiedDeviceId;

37
  /// A user-specified device ID.
38 39 40 41 42
  String get specifiedDeviceId {
    if (_specifiedDeviceId == null || _specifiedDeviceId == 'all')
      return null;
    return _specifiedDeviceId;
  }
43

44 45 46 47 48
  set specifiedDeviceId(String id) {
    _specifiedDeviceId = id;
  }

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

51 52 53 54
  /// True when the user has specified all devices by setting
  /// specifiedDeviceId = 'all'.
  bool get hasSpecifiedAllDevices => _specifiedDeviceId == 'all';

55
  Stream<Device> getDevicesById(String deviceId) async* {
56
    final List<Device> devices = await getAllConnectedDevices().toList();
Devon Carew's avatar
Devon Carew committed
57
    deviceId = deviceId.toLowerCase();
58 59 60 61 62 63 64
    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);

65 66
    final Device exactMatch = devices.firstWhere(
        exactlyMatchesDeviceId, orElse: () => null);
67 68 69 70
    if (exactMatch != null) {
      yield exactMatch;
      return;
    }
71

72
    // Match on a id or name starting with [deviceId].
73
    for (Device device in devices.where(startsWithDeviceId))
74
      yield device;
Devon Carew's avatar
Devon Carew committed
75 76
  }

77
  /// Return the list of connected devices, filtered by any user-specified device id.
78 79 80 81
  Stream<Device> getDevices() {
    return hasSpecifiedDeviceId
        ? getDevicesById(specifiedDeviceId)
        : getAllConnectedDevices();
82 83
  }

84 85 86 87
  Iterable<DeviceDiscovery> get _platformDiscoverers {
    return _deviceDiscoverers.where((DeviceDiscovery discoverer) => discoverer.supportsPlatform);
  }

88
  /// Return the list of all connected devices.
89 90 91 92 93 94
  Stream<Device> getAllConnectedDevices() async* {
    for (DeviceDiscovery discoverer in _platformDiscoverers) {
      for (Device device in await discoverer.devices) {
        yield device;
      }
    }
95
  }
96 97 98 99 100 101 102 103 104 105 106 107 108 109

  /// 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;
  }
110 111 112 113 114
}

/// An abstract class to discover and enumerate a specific type of devices.
abstract class DeviceDiscovery {
  bool get supportsPlatform;
115 116 117 118 119

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

120
  Future<List<Device>> get devices;
121 122 123 124

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

127 128 129 130 131
/// A [DeviceDiscovery] implementation that uses polling to discover device adds
/// and removals.
abstract class PollingDeviceDiscovery extends DeviceDiscovery {
  PollingDeviceDiscovery(this.name);

132 133
  static const Duration _pollingInterval = Duration(seconds: 4);
  static const Duration _pollingTimeout = Duration(seconds: 30);
134 135 136

  final String name;
  ItemListNotifier<Device> _items;
137
  Poller _poller;
138

139
  Future<List<Device>> pollingGetDevices();
140 141

  void startPolling() {
142
    if (_poller == null) {
143
      _items ??= new ItemListNotifier<Device>();
144 145

      _poller = new Poller(() async {
146 147 148 149 150 151
        try {
          final List<Device> devices = await pollingGetDevices().timeout(_pollingTimeout);
          _items.updateWithNewList(devices);
        } on TimeoutException {
          printTrace('Device poll timed out.');
        }
152
      }, _pollingInterval);
153 154 155 156
    }
  }

  void stopPolling() {
157 158
    _poller?.cancel();
    _poller = null;
159 160
  }

161
  @override
162 163
  Future<List<Device>> get devices async {
    _items ??= new ItemListNotifier<Device>.from(await pollingGetDevices());
164 165 166 167
    return _items.items;
  }

  Stream<Device> get onAdded {
168
    _items ??= new ItemListNotifier<Device>();
169 170 171 172
    return _items.onAdded;
  }

  Stream<Device> get onRemoved {
173
    _items ??= new ItemListNotifier<Device>();
174 175 176 177 178
    return _items.onRemoved;
  }

  void dispose() => stopPolling();

179
  @override
180 181 182
  String toString() => '$name device discovery';
}

183
abstract class Device {
184
  Device(this.id);
185

186
  final String id;
187

188 189
  String get name;

190 191
  bool get supportsStartPaused => true;

192
  /// Whether it is an emulated device running on localhost.
193
  Future<bool> get isLocalEmulator;
194

195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
  /// 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;
    }
  }

214
  /// Check if a version of the given app is already installed
215
  Future<bool> isAppInstalled(ApplicationPackage app);
216

217
  /// Check if the latest build of the [app] is already installed.
218
  Future<bool> isLatestBuildInstalled(ApplicationPackage app);
219

220
  /// Install an app package on the current device
221
  Future<bool> installApp(ApplicationPackage app);
222

223
  /// Uninstall an app package from the current device
224
  Future<bool> uninstallApp(ApplicationPackage app);
225

226 227 228 229 230
  /// 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.
231
  String supportMessage() => isSupported() ? 'Supported' : 'Unsupported';
232

233 234
  /// The device's platform.
  Future<TargetPlatform> get targetPlatform;
235

236
  Future<String> get sdkNameAndVersion;
237

238 239 240 241
  /// 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.
  DeviceLogReader getLogReader({ApplicationPackage app});
242

243 244 245
  /// Get the port forwarder for this device.
  DevicePortForwarder get portForwarder;

246 247
  /// Clear the device's logs.
  void clearLogs();
248

249 250 251
  /// Start an app package on the current device.
  ///
  /// [platformArgs] allows callers to pass platform-specific arguments to the
252
  /// start call. The build mode is not used by all platforms.
253 254 255 256 257
  ///
  /// 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
258
  Future<LaunchResult> startApp(
259
    ApplicationPackage package, {
260 261
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
262
    DebuggingOptions debuggingOptions,
263
    Map<String, dynamic> platformArgs,
264 265 266 267
    bool prebuiltApplication = false,
    bool applicationNeedsRebuild = false,
    bool usesTerminalUi = true,
    bool ipv6 = false,
268
  });
269

270
  /// Does this device implement support for hot reloading / restarting?
271
  bool get supportsHotMode => true;
272

273
  /// Stop an app package on the current device.
274
  Future<bool> stopApp(ApplicationPackage app);
275

Devon Carew's avatar
Devon Carew committed
276 277
  bool get supportsScreenshot => false;

278
  Future<void> takeScreenshot(File outputFile) => new Future<Null>.error('unimplemented');
Devon Carew's avatar
Devon Carew committed
279

280 281 282
  // TODO(dantup): discoverApps is no longer used and can possibly be removed.
  // Waiting for a response here:
  // https://github.com/flutter/flutter/pull/18873#discussion_r198862179
283 284 285 286
  /// Find the apps that are currently running on this device.
  Future<List<DiscoveredApp>> discoverApps() =>
      new Future<List<DiscoveredApp>>.value(<DiscoveredApp>[]);

287
  @override
288 289
  int get hashCode => id.hashCode;

290
  @override
291 292 293 294 295 296 297 298
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! Device)
      return false;
    return id == other.id;
  }

299
  @override
300
  String toString() => name;
Devon Carew's avatar
Devon Carew committed
301

302
  static Stream<String> descriptions(List<Device> devices) async* {
303
    if (devices.isEmpty)
304
      return;
Devon Carew's avatar
Devon Carew committed
305

306
    // Extract device information
307
    final List<List<String>> table = <List<String>>[];
Devon Carew's avatar
Devon Carew committed
308 309
    for (Device device in devices) {
      String supportIndicator = device.isSupported() ? '' : ' (unsupported)';
310 311 312
      final TargetPlatform targetPlatform = await device.targetPlatform;
      if (await device.isLocalEmulator) {
        final String type = targetPlatform == TargetPlatform.ios ? 'simulator' : 'emulator';
313 314
        supportIndicator += ' ($type)';
      }
315 316 317
      table.add(<String>[
        device.name,
        device.id,
318 319
        '${getNameForTargetPlatform(targetPlatform)}',
        '${await device.sdkNameAndVersion}$supportIndicator',
320 321 322 323
      ]);
    }

    // Calculate column widths
324
    final List<int> indices = new List<int>.generate(table[0].length - 1, (int i) => i);
325 326 327 328 329 330
    List<int> widths = indices.map((int i) => 0).toList();
    for (List<String> row in table) {
      widths = indices.map((int i) => math.max(widths[i], row[i].length)).toList();
    }

    // Join columns into lines of text
331 332 333
    for (List<String> row in table) {
      yield indices.map((int i) => row[i].padRight(widths[i])).join(' • ') + ' • ${row.last}';
    }
334 335
  }

336 337
  static Future<Null> printDevices(List<Device> devices) async {
    await descriptions(devices).forEach(printStatus);
Devon Carew's avatar
Devon Carew committed
338
  }
339 340
}

Devon Carew's avatar
Devon Carew committed
341
class DebuggingOptions {
342
  DebuggingOptions.enabled(this.buildInfo, {
343 344 345 346 347
    this.startPaused = false,
    this.enableSoftwareRendering = false,
    this.skiaDeterministicRendering = false,
    this.traceSkia = false,
    this.useTestFonts = false,
Devon Carew's avatar
Devon Carew committed
348 349 350
    this.observatoryPort,
   }) : debuggingEnabled = true;

351
  DebuggingOptions.disabled(this.buildInfo) :
Devon Carew's avatar
Devon Carew committed
352
    debuggingEnabled = false,
353
    useTestFonts = false,
Devon Carew's avatar
Devon Carew committed
354
    startPaused = false,
355
    enableSoftwareRendering = false,
356
    skiaDeterministicRendering = false,
357
    traceSkia = false,
358
    observatoryPort = null;
Devon Carew's avatar
Devon Carew committed
359 360 361

  final bool debuggingEnabled;

362
  final BuildInfo buildInfo;
Devon Carew's avatar
Devon Carew committed
363
  final bool startPaused;
364
  final bool enableSoftwareRendering;
365
  final bool skiaDeterministicRendering;
366
  final bool traceSkia;
367
  final bool useTestFonts;
Devon Carew's avatar
Devon Carew committed
368 369 370 371 372 373
  final int observatoryPort;

  bool get hasObservatoryPort => observatoryPort != null;
}

class LaunchResult {
374 375
  LaunchResult.succeeded({ this.observatoryUri }) : started = true;
  LaunchResult.failed() : started = false, observatoryUri = null;
Devon Carew's avatar
Devon Carew committed
376

377
  bool get hasObservatory => observatoryUri != null;
Devon Carew's avatar
Devon Carew committed
378 379

  final bool started;
380
  final Uri observatoryUri;
Devon Carew's avatar
Devon Carew committed
381 382 383

  @override
  String toString() {
384
    final StringBuffer buf = new StringBuffer('started=$started');
385 386
    if (observatoryUri != null)
      buf.write(', observatory=$observatoryUri');
Devon Carew's avatar
Devon Carew committed
387 388 389 390
    return buf.toString();
  }
}

391
class ForwardedPort {
392 393
  ForwardedPort(this.hostPort, this.devicePort) : context = null;
  ForwardedPort.withContext(this.hostPort, this.devicePort, this.context);
394 395 396

  final int hostPort;
  final int devicePort;
397
  final dynamic context;
398

399
  @override
400 401 402 403 404 405 406 407 408 409
  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.
410
  /// If [hostPort] is null or zero, will auto select a host port.
411
  /// Returns a Future that completes with the host port.
412
  Future<int> forward(int devicePort, {int hostPort});
413 414

  /// Stops forwarding [forwardedPort].
Ian Hickson's avatar
Ian Hickson committed
415
  Future<Null> unforward(ForwardedPort forwardedPort);
416 417
}

Devon Carew's avatar
Devon Carew committed
418
/// Read the log for a particular device.
Devon Carew's avatar
Devon Carew committed
419 420 421
abstract class DeviceLogReader {
  String get name;

Devon Carew's avatar
Devon Carew committed
422 423
  /// 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
424

425
  @override
Devon Carew's avatar
Devon Carew committed
426
  String toString() => name;
427

428
  /// Process ID of the app on the device.
429
  int appPid;
Devon Carew's avatar
Devon Carew committed
430
}
431 432 433

/// Describes an app running on the device.
class DiscoveredApp {
434
  DiscoveredApp(this.id, this.observatoryPort);
435 436 437
  final String id;
  final int observatoryPort;
}