device.dart 12.5 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/common.dart';
11
import 'base/context.dart';
12
import 'base/file_system.dart';
13
import 'base/port_scanner.dart';
14
import 'base/utils.dart';
15
import 'build_info.dart';
Devon Carew's avatar
Devon Carew committed
16
import 'globals.dart';
17 18
import 'ios/devices.dart';
import 'ios/simulators.dart';
19

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

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

/// An abstract class to discover and enumerate a specific type of devices.
abstract class DeviceDiscovery {
  bool get supportsPlatform;
101 102 103 104 105

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

106
  Future<List<Device>> get devices;
107 108
}

109 110 111 112 113
/// A [DeviceDiscovery] implementation that uses polling to discover device adds
/// and removals.
abstract class PollingDeviceDiscovery extends DeviceDiscovery {
  PollingDeviceDiscovery(this.name);

114 115
  static const Duration _pollingInterval = const Duration(seconds: 4);
  static const Duration _pollingTimeout = const Duration(seconds: 30);
116 117 118

  final String name;
  ItemListNotifier<Device> _items;
119
  Poller _poller;
120

121
  Future<List<Device>> pollingGetDevices();
122 123

  void startPolling() {
124
    if (_poller == null) {
125
      _items ??= new ItemListNotifier<Device>();
126 127

      _poller = new Poller(() async {
128 129 130 131 132 133
        try {
          final List<Device> devices = await pollingGetDevices().timeout(_pollingTimeout);
          _items.updateWithNewList(devices);
        } on TimeoutException {
          printTrace('Device poll timed out.');
        }
134
      }, _pollingInterval);
135 136 137 138
    }
  }

  void stopPolling() {
139 140
    _poller?.cancel();
    _poller = null;
141 142
  }

143
  @override
144 145
  Future<List<Device>> get devices async {
    _items ??= new ItemListNotifier<Device>.from(await pollingGetDevices());
146 147 148 149
    return _items.items;
  }

  Stream<Device> get onAdded {
150
    _items ??= new ItemListNotifier<Device>();
151 152 153 154
    return _items.onAdded;
  }

  Stream<Device> get onRemoved {
155
    _items ??= new ItemListNotifier<Device>();
156 157 158 159 160
    return _items.onRemoved;
  }

  void dispose() => stopPolling();

161
  @override
162 163 164
  String toString() => '$name device discovery';
}

165
abstract class Device {
166
  Device(this.id);
167

168
  final String id;
169

170 171
  String get name;

172 173
  bool get supportsStartPaused => true;

174
  /// Whether it is an emulated device running on localhost.
175
  Future<bool> get isLocalEmulator;
176

177
  /// Check if a version of the given app is already installed
178
  Future<bool> isAppInstalled(ApplicationPackage app);
179

180
  /// Check if the latest build of the [app] is already installed.
181
  Future<bool> isLatestBuildInstalled(ApplicationPackage app);
182

183
  /// Install an app package on the current device
184
  Future<bool> installApp(ApplicationPackage app);
185

186
  /// Uninstall an app package from the current device
187
  Future<bool> uninstallApp(ApplicationPackage app);
188

189 190 191 192 193
  /// 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.
194
  String supportMessage() => isSupported() ? 'Supported' : 'Unsupported';
195

196 197
  /// The device's platform.
  Future<TargetPlatform> get targetPlatform;
198

199
  Future<String> get sdkNameAndVersion;
200

201 202 203 204
  /// 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});
205

206 207 208
  /// Get the port forwarder for this device.
  DevicePortForwarder get portForwarder;

209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
  Future<int> forwardPort(int devicePort, {int hostPort}) async {
    try {
      hostPort = await portForwarder
          .forward(devicePort, hostPort: hostPort)
          .timeout(const Duration(seconds: 60), onTimeout: () {
            throw new ToolExit(
                'Timeout while atempting to foward device port $devicePort');
          });
      printTrace('Forwarded host port $hostPort to device port $devicePort');
      return hostPort;
    } catch (e) {
      throw new ToolExit(
          'Unable to forward host port $hostPort to device port $devicePort: $e');
    }
  }

225 226
  /// Clear the device's logs.
  void clearLogs();
227

228 229 230
  /// Start an app package on the current device.
  ///
  /// [platformArgs] allows callers to pass platform-specific arguments to the
231
  /// start call. The build mode is not used by all platforms.
232 233 234 235 236
  ///
  /// 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
237
  Future<LaunchResult> startApp(
238
    ApplicationPackage package, {
239 240
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
241
    DebuggingOptions debuggingOptions,
242
    Map<String, dynamic> platformArgs,
243
    bool prebuiltApplication: false,
244 245
    bool applicationNeedsRebuild: false,
    bool usesTerminalUi: true,
246
  });
247

248
  /// Does this device implement support for hot reloading / restarting?
249
  bool get supportsHotMode => true;
250

251
  /// Stop an app package on the current device.
252
  Future<bool> stopApp(ApplicationPackage app);
253

Devon Carew's avatar
Devon Carew committed
254 255
  bool get supportsScreenshot => false;

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

258 259 260 261
  /// Find the apps that are currently running on this device.
  Future<List<DiscoveredApp>> discoverApps() =>
      new Future<List<DiscoveredApp>>.value(<DiscoveredApp>[]);

262
  @override
263 264
  int get hashCode => id.hashCode;

265
  @override
266 267 268 269 270 271 272 273
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! Device)
      return false;
    return id == other.id;
  }

274
  @override
275
  String toString() => name;
Devon Carew's avatar
Devon Carew committed
276

277
  static Stream<String> descriptions(List<Device> devices) async* {
278
    if (devices.isEmpty)
279
      return;
Devon Carew's avatar
Devon Carew committed
280

281
    // Extract device information
282
    final List<List<String>> table = <List<String>>[];
Devon Carew's avatar
Devon Carew committed
283 284
    for (Device device in devices) {
      String supportIndicator = device.isSupported() ? '' : ' (unsupported)';
285 286 287
      final TargetPlatform targetPlatform = await device.targetPlatform;
      if (await device.isLocalEmulator) {
        final String type = targetPlatform == TargetPlatform.ios ? 'simulator' : 'emulator';
288 289
        supportIndicator += ' ($type)';
      }
290 291 292
      table.add(<String>[
        device.name,
        device.id,
293 294
        '${getNameForTargetPlatform(targetPlatform)}',
        '${await device.sdkNameAndVersion}$supportIndicator',
295 296 297 298
      ]);
    }

    // Calculate column widths
299
    final List<int> indices = new List<int>.generate(table[0].length - 1, (int i) => i);
300 301 302 303 304 305
    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
306 307 308
    for (List<String> row in table) {
      yield indices.map((int i) => row[i].padRight(widths[i])).join(' • ') + ' • ${row.last}';
    }
309 310
  }

311 312
  static Future<Null> printDevices(List<Device> devices) async {
    await descriptions(devices).forEach(printStatus);
Devon Carew's avatar
Devon Carew committed
313
  }
314 315
}

Devon Carew's avatar
Devon Carew committed
316
class DebuggingOptions {
317
  DebuggingOptions.enabled(this.buildInfo, {
Devon Carew's avatar
Devon Carew committed
318
    this.startPaused: false,
319
    this.enableSoftwareRendering: false,
320
    this.traceSkia: false,
321
    this.useTestFonts: false,
Devon Carew's avatar
Devon Carew committed
322 323 324
    this.observatoryPort,
   }) : debuggingEnabled = true;

325
  DebuggingOptions.disabled(this.buildInfo) :
Devon Carew's avatar
Devon Carew committed
326
    debuggingEnabled = false,
327
    useTestFonts = false,
Devon Carew's avatar
Devon Carew committed
328
    startPaused = false,
329
    enableSoftwareRendering = false,
330
    traceSkia = false,
331
    observatoryPort = null;
Devon Carew's avatar
Devon Carew committed
332 333 334

  final bool debuggingEnabled;

335
  final BuildInfo buildInfo;
Devon Carew's avatar
Devon Carew committed
336
  final bool startPaused;
337
  final bool enableSoftwareRendering;
338
  final bool traceSkia;
339
  final bool useTestFonts;
Devon Carew's avatar
Devon Carew committed
340 341 342 343 344
  final int observatoryPort;

  bool get hasObservatoryPort => observatoryPort != null;

  /// Return the user specified observatory port. If that isn't available,
345
  /// return [kDefaultObservatoryPort], or a port close to that one.
Devon Carew's avatar
Devon Carew committed
346 347 348
  Future<int> findBestObservatoryPort() {
    if (hasObservatoryPort)
      return new Future<int>.value(observatoryPort);
349
    return portScanner.findPreferredPort(observatoryPort ?? kDefaultObservatoryPort);
Devon Carew's avatar
Devon Carew committed
350 351 352 353
  }
}

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

357
  bool get hasObservatory => observatoryUri != null;
Devon Carew's avatar
Devon Carew committed
358 359

  final bool started;
360
  final Uri observatoryUri;
Devon Carew's avatar
Devon Carew committed
361 362 363

  @override
  String toString() {
364
    final StringBuffer buf = new StringBuffer('started=$started');
365 366
    if (observatoryUri != null)
      buf.write(', observatory=$observatoryUri');
Devon Carew's avatar
Devon Carew committed
367 368 369 370
    return buf.toString();
  }
}

371
class ForwardedPort {
372 373
  ForwardedPort(this.hostPort, this.devicePort) : context = null;
  ForwardedPort.withContext(this.hostPort, this.devicePort, this.context);
374 375 376

  final int hostPort;
  final int devicePort;
377
  final dynamic context;
378

379
  @override
380 381 382 383 384 385 386 387 388 389 390 391
  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.
  /// If [hostPort] is null, will auto select a host port.
  /// Returns a Future that completes with the host port.
392
  Future<int> forward(int devicePort, { int hostPort });
393 394

  /// Stops forwarding [forwardedPort].
Ian Hickson's avatar
Ian Hickson committed
395
  Future<Null> unforward(ForwardedPort forwardedPort);
396 397
}

Devon Carew's avatar
Devon Carew committed
398
/// Read the log for a particular device.
Devon Carew's avatar
Devon Carew committed
399 400 401
abstract class DeviceLogReader {
  String get name;

Devon Carew's avatar
Devon Carew committed
402 403
  /// 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
404

405
  @override
Devon Carew's avatar
Devon Carew committed
406
  String toString() => name;
407 408 409

  /// Process ID of the app on the deivce.
  int appPid;
Devon Carew's avatar
Devon Carew committed
410
}
411 412 413

/// Describes an app running on the device.
class DiscoveredApp {
414
  DiscoveredApp(this.id, this.observatoryPort);
415 416 417
  final String id;
  final int observatoryPort;
}