import 'dart:async';
import 'dart:math' as math;
import 'package:meta/meta.dart';
import 'android/android_device.dart';
import 'application_package.dart';
import 'artifacts.dart';
import 'base/context.dart';
import 'base/file_system.dart';
import 'base/utils.dart';
import 'build_info.dart';
import 'fuchsia/fuchsia_device.dart';
import 'globals.dart';
import 'ios/devices.dart';
import 'ios/simulators.dart';
import 'linux/linux_device.dart';
import 'macos/macos_device.dart';
import 'project.dart';
import 'tester/flutter_tester.dart';
import 'web/web_device.dart';
import 'windows/windows_device.dart';
DeviceManager get deviceManager => context.get<DeviceManager>();
class Category {
const Category._(this.value);
static const Category web = Category._('web');
static const Category desktop = Category._('desktop');
static const Category mobile = Category._('mobile');
final String value;
@override
String toString() => value;
}
class PlatformType {
const PlatformType._(this.value);
static const PlatformType web = PlatformType._('web');
static const PlatformType android = PlatformType._('android');
static const PlatformType ios = PlatformType._('ios');
static const PlatformType linux = PlatformType._('linux');
static const PlatformType macos = PlatformType._('macos');
static const PlatformType windows = PlatformType._('windows');
static const PlatformType fuchsia = PlatformType._('fuchsia');
final String value;
@override
String toString() => value;
}
class DeviceManager {
List<DeviceDiscovery> get deviceDiscoverers => _deviceDiscoverers;
final List<DeviceDiscovery> _deviceDiscoverers = List<DeviceDiscovery>.unmodifiable(<DeviceDiscovery>[
AndroidDevices(),
IOSDevices(),
IOSSimulators(),
FuchsiaDevices(),
FlutterTesterDevices(),
MacOSDevices(),
LinuxDevices(),
WindowsDevices(),
WebDevices(),
]);
String _specifiedDeviceId;
String get specifiedDeviceId {
if (_specifiedDeviceId == null || _specifiedDeviceId == 'all')
return null;
return _specifiedDeviceId;
}
set specifiedDeviceId(String id) {
_specifiedDeviceId = id;
}
bool get hasSpecifiedDeviceId => specifiedDeviceId != null;
bool get hasSpecifiedAllDevices => _specifiedDeviceId == 'all';
Stream<Device> getDevicesById(String deviceId) async* {
final List<Device> devices = await getAllConnectedDevices().toList();
deviceId = deviceId.toLowerCase();
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);
final Device exactMatch = devices.firstWhere(
exactlyMatchesDeviceId, orElse: () => null);
if (exactMatch != null) {
yield exactMatch;
return;
}
for (Device device in devices.where(startsWithDeviceId))
yield device;
}
Stream<Device> getDevices() {
return hasSpecifiedDeviceId
? getDevicesById(specifiedDeviceId)
: getAllConnectedDevices();
}
Iterable<DeviceDiscovery> get _platformDiscoverers {
return deviceDiscoverers.where((DeviceDiscovery discoverer) => discoverer.supportsPlatform);
}
Stream<Device> getAllConnectedDevices() async* {
for (DeviceDiscovery discoverer in _platformDiscoverers) {
for (Device device in await discoverer.devices) {
yield device;
}
}
}
bool get canListAnything {
return _platformDiscoverers.any((DeviceDiscovery discoverer) => discoverer.canListAnything);
}
Future<List<String>> getDeviceDiagnostics() async {
return <String>[
for (DeviceDiscovery discoverer in _platformDiscoverers)
...await discoverer.getDiagnostics(),
];
}
Future<List<Device>> findTargetDevices(FlutterProject flutterProject) async {
List<Device> devices = await getDevices().toList();
if (hasSpecifiedAllDevices) {
devices = <Device>[
for (Device device in devices)
if (await device.targetPlatform != TargetPlatform.fuchsia &&
await device.targetPlatform != TargetPlatform.web_javascript)
device
];
}
if (devices.length > 1 && !hasSpecifiedDeviceId) {
devices = <Device>[
for (Device device in devices)
if (isDeviceSupportedForProject(device, flutterProject))
device
];
}
if (devices.length > 1 && !hasSpecifiedAllDevices) {
if (devices.any((Device device) => device.ephemeral == true)) {
devices = devices
.where((Device device) => device.ephemeral == true)
.toList();
}
}
return devices;
}
bool isDeviceSupportedForProject(Device device, FlutterProject flutterProject) {
return device.isSupportedForProject(flutterProject);
}
}
abstract class DeviceDiscovery {
bool get supportsPlatform;
bool get canListAnything;
Future<List<Device>> get devices;
Future<List<String>> getDiagnostics() => Future<List<String>>.value(<String>[]);
}
abstract class PollingDeviceDiscovery extends DeviceDiscovery {
PollingDeviceDiscovery(this.name);
static const Duration _pollingInterval = Duration(seconds: 4);
static const Duration _pollingTimeout = Duration(seconds: 30);
final String name;
ItemListNotifier<Device> _items;
Poller _poller;
Future<List<Device>> pollingGetDevices();
void startPolling() {
if (_poller == null) {
_items ??= ItemListNotifier<Device>();
_poller = Poller(() async {
try {
final List<Device> devices = await pollingGetDevices().timeout(_pollingTimeout);
_items.updateWithNewList(devices);
} on TimeoutException {
printTrace('Device poll timed out. Will retry.');
}
}, _pollingInterval);
}
}
void stopPolling() {
_poller?.cancel();
_poller = null;
}
@override
Future<List<Device>> get devices async {
_items ??= ItemListNotifier<Device>.from(await pollingGetDevices());
return _items.items;
}
Stream<Device> get onAdded {
_items ??= ItemListNotifier<Device>();
return _items.onAdded;
}
Stream<Device> get onRemoved {
_items ??= ItemListNotifier<Device>();
return _items.onRemoved;
}
void dispose() => stopPolling();
@override
String toString() => '$name device discovery';
}
abstract class Device {
Device(this.id, {@required this.category, @required this.platformType, @required this.ephemeral});
final String id;
final Category category;
final PlatformType platformType;
final bool ephemeral;
String get name;
bool get supportsStartPaused => true;
Future<bool> get isLocalEmulator;
Future<String> get emulatorId;
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;
}
}
bool isSupportedForProject(FlutterProject flutterProject);
Future<bool> isAppInstalled(ApplicationPackage app);
Future<bool> isLatestBuildInstalled(ApplicationPackage app);
Future<bool> installApp(ApplicationPackage app);
Future<bool> uninstallApp(ApplicationPackage app);
bool isSupported();
String supportMessage() => isSupported() ? 'Supported' : 'Unsupported';
Future<TargetPlatform> get targetPlatform;
Future<String> get sdkNameAndVersion;
DeviceLogReader getLogReader({ ApplicationPackage app });
DevicePortForwarder get portForwarder;
void clearLogs();
OverrideArtifacts get artifactOverrides => null;
Future<LaunchResult> startApp(
ApplicationPackage package, {
String mainPath,
String route,
DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs,
bool prebuiltApplication = false,
bool ipv6 = false,
bool usesTerminalUi = true,
});
bool get supportsHotReload => true;
bool get supportsHotRestart => true;
bool get supportsFlutterExit => true;
bool get supportsScreenshot => false;
Future<bool> stopApp(ApplicationPackage app);
Future<void> takeScreenshot(File outputFile) => Future<void>.error('unimplemented');
@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 Stream<String> descriptions(List<Device> devices) async* {
if (devices.isEmpty)
return;
final List<List<String>> table = <List<String>>[];
for (Device device in devices) {
String supportIndicator = device.isSupported() ? '' : ' (unsupported)';
final TargetPlatform targetPlatform = await device.targetPlatform;
if (await device.isLocalEmulator) {
final String type = targetPlatform == TargetPlatform.ios ? 'simulator' : 'emulator';
supportIndicator += ' ($type)';
}
table.add(<String>[
device.name,
device.id,
'${getNameForTargetPlatform(targetPlatform)}',
'${await device.sdkNameAndVersion}$supportIndicator',
]);
}
final List<int> indices = List<int>.generate(table[0].length - 1, (int i) => i);
List<int> widths = indices.map<int>((int i) => 0).toList();
for (List<String> row in table) {
widths = indices.map<int>((int i) => math.max(widths[i], row[i].length)).toList();
}
for (List<String> row in table) {
yield indices.map<String>((int i) => row[i].padRight(widths[i])).join(' • ') + ' • ${row.last}';
}
}
static Future<void> printDevices(List<Device> devices) async {
await descriptions(devices).forEach(printStatus);
}
}
class DebuggingOptions {
DebuggingOptions.enabled(
this.buildInfo, {
this.startPaused = false,
this.disableServiceAuthCodes = false,
this.dartFlags = '',
this.enableSoftwareRendering = false,
this.skiaDeterministicRendering = false,
this.traceSkia = false,
this.traceSystrace = false,
this.dumpSkpOnShaderCompilation = false,
this.useTestFonts = false,
this.verboseSystemLogs = false,
this.observatoryPort,
}) : debuggingEnabled = true;
DebuggingOptions.disabled(this.buildInfo)
: debuggingEnabled = false,
useTestFonts = false,
startPaused = false,
dartFlags = '',
disableServiceAuthCodes = false,
enableSoftwareRendering = false,
skiaDeterministicRendering = false,
traceSkia = false,
traceSystrace = false,
dumpSkpOnShaderCompilation = false,
verboseSystemLogs = false,
observatoryPort = null;
final bool debuggingEnabled;
final BuildInfo buildInfo;
final bool startPaused;
final String dartFlags;
final bool disableServiceAuthCodes;
final bool enableSoftwareRendering;
final bool skiaDeterministicRendering;
final bool traceSkia;
final bool traceSystrace;
final bool dumpSkpOnShaderCompilation;
final bool useTestFonts;
final bool verboseSystemLogs;
final int observatoryPort;
bool get hasObservatoryPort => observatoryPort != null;
}
class LaunchResult {
LaunchResult.succeeded({ this.observatoryUri }) : started = true;
LaunchResult.failed()
: started = false,
observatoryUri = null;
bool get hasObservatory => observatoryUri != null;
final bool started;
final Uri observatoryUri;
@override
String toString() {
final StringBuffer buf = StringBuffer('started=$started');
if (observatoryUri != null)
buf.write(', observatory=$observatoryUri');
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';
}
abstract class DevicePortForwarder {
List<ForwardedPort> get forwardedPorts;
Future<int> forward(int devicePort, { int hostPort });
Future<void> unforward(ForwardedPort forwardedPort);
}
abstract class DeviceLogReader {
String get name;
Stream<String> get logLines;
@override
String toString() => name;
int appPid;
}
class DiscoveredApp {
DiscoveredApp(this.id, this.observatoryPort);
final String id;
final int observatoryPort;
}
class NoOpDeviceLogReader implements DeviceLogReader {
NoOpDeviceLogReader(this.name);
@override
final String name;
@override
int appPid;
@override
Stream<String> get logLines => const Stream<String>.empty();
}
class NoOpDevicePortForwarder implements DevicePortForwarder {
const NoOpDevicePortForwarder();
@override
Future<int> forward(int devicePort, { int hostPort }) async => devicePort;
@override
List<ForwardedPort> get forwardedPorts => <ForwardedPort>[];
@override
Future<void> unforward(ForwardedPort forwardedPort) async { }
}