// 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. import 'dart:async'; import 'dart:math' as math; import 'android/android_device.dart'; import 'application_package.dart'; import 'base/common.dart'; import 'base/context.dart'; import 'base/file_system.dart'; import 'base/os.dart'; import 'base/utils.dart'; import 'build_info.dart'; import 'globals.dart'; import 'ios/devices.dart'; import 'ios/simulators.dart'; DeviceManager get deviceManager => context[DeviceManager]; /// A class to get all available devices. class DeviceManager { /// Constructing DeviceManagers is cheap; they only do expensive work if some /// of their methods are called. DeviceManager() { // Register the known discoverers. _deviceDiscoverers.add(new AndroidDevices()); _deviceDiscoverers.add(new IOSDevices()); _deviceDiscoverers.add(new IOSSimulators()); } List<DeviceDiscovery> _deviceDiscoverers = <DeviceDiscovery>[]; /// A user-specified device ID. String specifiedDeviceId; bool get hasSpecifiedDeviceId => specifiedDeviceId != null; /// Return the devices with a name or id matching [deviceId]. /// This does a case insentitive compare with [deviceId]. Future<List<Device>> getDevicesById(String deviceId) async { deviceId = deviceId.toLowerCase(); List<Device> devices = await getAllConnectedDevices(); Device device = devices.firstWhere( (Device device) => device.id.toLowerCase() == deviceId || device.name.toLowerCase() == deviceId, orElse: () => null); if (device != null) return <Device>[device]; // Match on a id or name starting with [deviceId]. return devices.where((Device device) { return (device.id.toLowerCase().startsWith(deviceId) || device.name.toLowerCase().startsWith(deviceId)); }).toList(); } /// Return the list of connected devices, filtered by any user-specified device id. Future<List<Device>> getDevices() async { if (specifiedDeviceId == null) { return getAllConnectedDevices(); } else { return getDevicesById(specifiedDeviceId); } } /// Return the list of all connected devices. Future<List<Device>> getAllConnectedDevices() async { return _deviceDiscoverers .where((DeviceDiscovery discoverer) => discoverer.supportsPlatform) .expand((DeviceDiscovery discoverer) => discoverer.devices) .toList(); } } /// An abstract class to discover and enumerate a specific type of devices. abstract class DeviceDiscovery { bool get supportsPlatform; List<Device> get devices; } /// A [DeviceDiscovery] implementation that uses polling to discover device adds /// and removals. abstract class PollingDeviceDiscovery extends DeviceDiscovery { PollingDeviceDiscovery(this.name); static const Duration _pollingDuration = const Duration(seconds: 4); final String name; ItemListNotifier<Device> _items; Timer _timer; List<Device> pollingGetDevices(); void startPolling() { if (_timer == null) { if (_items == null) _items = new ItemListNotifier<Device>(); _timer = new Timer.periodic(_pollingDuration, (Timer timer) { _items.updateWithNewList(pollingGetDevices()); }); } } void stopPolling() { _timer?.cancel(); _timer = null; } @override List<Device> get devices { if (_items == null) _items = new ItemListNotifier<Device>.from(pollingGetDevices()); return _items.items; } Stream<Device> get onAdded { if (_items == null) _items = new ItemListNotifier<Device>(); return _items.onAdded; } Stream<Device> get onRemoved { if (_items == null) _items = new ItemListNotifier<Device>(); return _items.onRemoved; } void dispose() => stopPolling(); @override String toString() => '$name device discovery'; } abstract class Device { Device(this.id); final String id; String get name; bool get supportsStartPaused => true; /// Whether it is an emulated device running on localhost. bool get isLocalEmulator; /// Check if a version of the given app is already installed bool isAppInstalled(ApplicationPackage app); /// Install an app package on the current device bool installApp(ApplicationPackage app); /// Uninstall an app package from the current device bool uninstallApp(ApplicationPackage app); /// 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. String supportMessage() => isSupported() ? "Supported" : "Unsupported"; // TODO(tvolkert): Rename to `targetPlatform`, and remove the "as p" // aliases on the `platform.dart` imports where applicable. TargetPlatform get platform; String get sdkNameAndVersion; /// 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}); /// Get the port forwarder for this device. DevicePortForwarder get portForwarder; 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'); } } /// Clear the device's logs. void clearLogs(); /// Start an app package on the current device. /// /// [platformArgs] allows callers to pass platform-specific arguments to the /// start call. The build mode is not used by all platforms. Future<LaunchResult> startApp( ApplicationPackage package, BuildMode mode, { String mainPath, String route, DebuggingOptions debuggingOptions, Map<String, dynamic> platformArgs, bool prebuiltApplication: false, bool applicationNeedsRebuild: false }); /// Does this device implement support for hot reloading / restarting? bool get supportsHotMode => true; /// Stop an app package on the current device. Future<bool> stopApp(ApplicationPackage app); bool get supportsScreenshot => false; Future<Null> takeScreenshot(File outputFile) => new Future<Null>.error('unimplemented'); /// Find the apps that are currently running on this device. Future<List<DiscoveredApp>> discoverApps() => new Future<List<DiscoveredApp>>.value(<DiscoveredApp>[]); @override int get hashCode => id.hashCode; @override bool operator ==(dynamic other) { if (identical(this, other)) return true; if (other is! Device) return false; return id == other.id; } @override String toString() => name; static Iterable<String> descriptions(List<Device> devices) { if (devices.isEmpty) return <String>[]; // Extract device information List<List<String>> table = <List<String>>[]; for (Device device in devices) { String supportIndicator = device.isSupported() ? '' : ' (unsupported)'; if (device.isLocalEmulator) { String type = device.platform == TargetPlatform.ios ? 'simulator' : 'emulator'; supportIndicator += ' ($type)'; } table.add(<String>[ device.name, device.id, '${getNameForTargetPlatform(device.platform)}', '${device.sdkNameAndVersion}$supportIndicator', ]); } // Calculate column widths List<int> indices = new List<int>.generate(table[0].length - 1, (int i) => i); 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 return table.map((List<String> row) => indices.map((int i) => row[i].padRight(widths[i])).join(' • ') + ' • ${row.last}'); } static void printDevices(List<Device> devices) { descriptions(devices).forEach((String msg) => printStatus(msg)); } } class DebuggingOptions { DebuggingOptions.enabled(this.buildMode, { this.startPaused: false, this.observatoryPort, this.diagnosticPort }) : debuggingEnabled = true; DebuggingOptions.disabled(this.buildMode) : debuggingEnabled = false, startPaused = false, observatoryPort = null, diagnosticPort = null; final bool debuggingEnabled; final BuildMode buildMode; final bool startPaused; final int observatoryPort; final int diagnosticPort; bool get hasObservatoryPort => observatoryPort != null; /// Return the user specified observatory port. If that isn't available, /// return [kDefaultObservatoryPort], or a port close to that one. Future<int> findBestObservatoryPort() { if (hasObservatoryPort) return new Future<int>.value(observatoryPort); return findPreferredPort(observatoryPort ?? kDefaultObservatoryPort); } bool get hasDiagnosticPort => diagnosticPort != null; /// Return the user specified diagnostic port. If that isn't available, /// return [kDefaultDiagnosticPort], or a port close to that one. Future<int> findBestDiagnosticPort() { if (hasDiagnosticPort) return new Future<int>.value(diagnosticPort); return findPreferredPort(diagnosticPort ?? kDefaultDiagnosticPort); } } class LaunchResult { LaunchResult.succeeded({ this.observatoryUri, this.diagnosticUri }) : started = true; LaunchResult.failed() : started = false, observatoryUri = null, diagnosticUri = null; bool get hasObservatory => observatoryUri != null; final bool started; final Uri observatoryUri; final Uri diagnosticUri; @override String toString() { StringBuffer buf = new StringBuffer('started=$started'); if (observatoryUri != null) buf.write(', observatory=$observatoryUri'); if (diagnosticUri != null) buf.write(', diagnostic=$diagnosticUri'); return buf.toString(); } } class ForwardedPort { ForwardedPort(this.hostPort, this.devicePort) : context = null; ForwardedPort.withContext(this.hostPort, this.devicePort, this.context); final int hostPort; final int devicePort; final dynamic context; @override 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. Future<int> forward(int devicePort, { int hostPort }); /// Stops forwarding [forwardedPort]. Future<Null> unforward(ForwardedPort forwardedPort); } /// Read the log for a particular device. abstract class DeviceLogReader { String get name; /// A broadcast stream where each element in the string is a line of log output. Stream<String> get logLines; @override String toString() => name; } /// Describes an app running on the device. class DiscoveredApp { DiscoveredApp(this.id, this.observatoryPort, this.diagnosticPort); final String id; final int observatoryPort; final int diagnosticPort; }