Merge pull request #2064 from abarth/snackbar_action
SnackBar should have a single optional action
diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart
index 3214a18..2fcfa45 100644
--- a/packages/flutter/lib/material.dart
+++ b/packages/flutter/lib/material.dart
@@ -7,7 +7,9 @@
/// See https://www.google.com/design/spec/material-design/introduction.html
library material;
+export 'src/material/app.dart';
export 'src/material/bottom_sheet.dart';
+export 'src/material/button.dart';
export 'src/material/card.dart';
export 'src/material/checkbox.dart';
export 'src/material/chip.dart';
@@ -33,11 +35,9 @@
export 'src/material/icon_theme_data.dart';
export 'src/material/ink_well.dart';
export 'src/material/input.dart';
+export 'src/material/list.dart';
export 'src/material/list_item.dart';
export 'src/material/material.dart';
-export 'src/material/material_app.dart';
-export 'src/material/material_button.dart';
-export 'src/material/material_list.dart';
export 'src/material/page.dart';
export 'src/material/popup_menu.dart';
export 'src/material/progress_indicator.dart';
diff --git a/packages/flutter/lib/src/animation/animation_controller.dart b/packages/flutter/lib/src/animation/animation_controller.dart
index 124aa31..b6846a9 100644
--- a/packages/flutter/lib/src/animation/animation_controller.dart
+++ b/packages/flutter/lib/src/animation/animation_controller.dart
@@ -163,10 +163,11 @@
AnimationStatus _lastStatus = AnimationStatus.dismissed;
void _checkStatusChanged() {
- AnimationStatus currentStatus = status;
- if (currentStatus != _lastStatus)
- notifyStatusListeners(status);
- _lastStatus = currentStatus;
+ AnimationStatus newStatus = status;
+ AnimationStatus oldStatus = _lastStatus;
+ _lastStatus = newStatus;
+ if (oldStatus != newStatus)
+ notifyStatusListeners(newStatus);
}
/// Drives the animation from its current value to target.
diff --git a/packages/flutter/lib/src/material/material_app.dart b/packages/flutter/lib/src/material/app.dart
similarity index 100%
rename from packages/flutter/lib/src/material/material_app.dart
rename to packages/flutter/lib/src/material/app.dart
diff --git a/packages/flutter/lib/src/material/material_button.dart b/packages/flutter/lib/src/material/button.dart
similarity index 100%
rename from packages/flutter/lib/src/material/material_button.dart
rename to packages/flutter/lib/src/material/button.dart
diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart
index dae65a5..53bda7d 100644
--- a/packages/flutter/lib/src/material/dialog.dart
+++ b/packages/flutter/lib/src/material/dialog.dart
@@ -6,8 +6,8 @@
import 'package:flutter/widgets.dart';
+import 'button.dart';
import 'colors.dart';
-import 'material_button.dart';
import 'material.dart';
import 'theme.dart';
diff --git a/packages/flutter/lib/src/material/flat_button.dart b/packages/flutter/lib/src/material/flat_button.dart
index 5a6a7cc..d469525 100644
--- a/packages/flutter/lib/src/material/flat_button.dart
+++ b/packages/flutter/lib/src/material/flat_button.dart
@@ -4,7 +4,7 @@
import 'package:flutter/widgets.dart';
-import 'material_button.dart';
+import 'button.dart';
import 'theme.dart';
/// A material design "flat button".
diff --git a/packages/flutter/lib/src/material/material_list.dart b/packages/flutter/lib/src/material/list.dart
similarity index 100%
rename from packages/flutter/lib/src/material/material_list.dart
rename to packages/flutter/lib/src/material/list.dart
diff --git a/packages/flutter/lib/src/material/raised_button.dart b/packages/flutter/lib/src/material/raised_button.dart
index e54fac7..11a9dcc 100644
--- a/packages/flutter/lib/src/material/raised_button.dart
+++ b/packages/flutter/lib/src/material/raised_button.dart
@@ -4,8 +4,8 @@
import 'package:flutter/widgets.dart';
+import 'button.dart';
import 'colors.dart';
-import 'material_button.dart';
import 'theme.dart';
/// A material design "raised button".
diff --git a/packages/flutter/lib/src/material/snack_bar.dart b/packages/flutter/lib/src/material/snack_bar.dart
index 64e4b58..20cde5b 100644
--- a/packages/flutter/lib/src/material/snack_bar.dart
+++ b/packages/flutter/lib/src/material/snack_bar.dart
@@ -4,11 +4,11 @@
import 'package:flutter/widgets.dart';
+import 'button.dart';
import 'flat_button.dart';
import 'material.dart';
-import 'material_button.dart';
-import 'theme.dart';
import 'theme_data.dart';
+import 'theme.dart';
import 'typography.dart';
// https://www.google.com/design/spec/components/snackbars-toasts.html#snackbars-toasts-specs
diff --git a/packages/flutter/lib/src/material/two_level_list.dart b/packages/flutter/lib/src/material/two_level_list.dart
index 0746b44..3a4a4cf 100644
--- a/packages/flutter/lib/src/material/two_level_list.dart
+++ b/packages/flutter/lib/src/material/two_level_list.dart
@@ -6,8 +6,8 @@
import 'colors.dart';
import 'icon.dart';
+import 'list.dart';
import 'list_item.dart';
-import 'material_list.dart';
import 'theme.dart';
import 'theme_data.dart';
diff --git a/packages/flutter/test/animation/animation_controller_test.dart b/packages/flutter/test/animation/animation_controller_test.dart
new file mode 100644
index 0000000..cb283f5
--- /dev/null
+++ b/packages/flutter/test/animation/animation_controller_test.dart
@@ -0,0 +1,40 @@
+// Copyright 2016 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 'package:flutter/animation.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:flutter/widgets.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test("Can set value during status callback", () {
+ WidgetFlutterBinding.ensureInitialized();
+ AnimationController controller = new AnimationController(
+ duration: const Duration(milliseconds: 100)
+ );
+ bool didComplete = false;
+ bool didDismiss = false;
+ controller.addStatusListener((AnimationStatus status) {
+ if (status == AnimationStatus.completed) {
+ didComplete = true;
+ controller.value = 0.0;
+ controller.forward();
+ } else if (status == AnimationStatus.dismissed) {
+ didDismiss = true;
+ controller.value = 0.0;
+ controller.forward();
+ }
+ });
+
+ controller.forward();
+ expect(didComplete, isFalse);
+ expect(didDismiss, isFalse);
+ Scheduler.instance.handleBeginFrame(const Duration(seconds: 1));
+ expect(didComplete, isFalse);
+ expect(didDismiss, isFalse);
+ Scheduler.instance.handleBeginFrame(const Duration(seconds: 2));
+ expect(didComplete, isTrue);
+ expect(didDismiss, isTrue);
+ });
+}
diff --git a/packages/flutter_tools/lib/src/android/adb.dart b/packages/flutter_tools/lib/src/android/adb.dart
index 2392dc3..377bca8 100644
--- a/packages/flutter_tools/lib/src/android/adb.dart
+++ b/packages/flutter_tools/lib/src/android/adb.dart
@@ -74,7 +74,7 @@
).toList();
}
- /// Listen to device activations and deactivations via the asb server's
+ /// Listen to device activations and deactivations via the adb server's
/// 'track-devices' command. Call cancel on the returned stream to stop
/// listening.
Stream<List<AdbDevice>> trackDevices() {
diff --git a/packages/flutter_tools/lib/src/android/device_android.dart b/packages/flutter_tools/lib/src/android/android_device.dart
similarity index 98%
rename from packages/flutter_tools/lib/src/android/device_android.dart
rename to packages/flutter_tools/lib/src/android/android_device.dart
index 404c0d8..f909666 100644
--- a/packages/flutter_tools/lib/src/android/device_android.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -29,17 +29,11 @@
// Path where the snapshot will be copied on the device.
const String _deviceSnapshotPath = '/data/local/tmp/dev_snapshot.bin';
-class AndroidDeviceDiscovery extends DeviceDiscovery {
- List<Device> _devices = <Device>[];
+class AndroidDevices extends PollingDeviceDiscovery {
+ AndroidDevices() : super('AndroidDevices');
bool get supportsPlatform => true;
-
- Future init() {
- _devices = getAdbDevices();
- return new Future.value();
- }
-
- List<Device> get devices => _devices;
+ List<Device> pollingGetDevices() => getAdbDevices();
}
class AndroidDevice extends Device {
diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart
new file mode 100644
index 0000000..b271fc1
--- /dev/null
+++ b/packages/flutter_tools/lib/src/base/utils.dart
@@ -0,0 +1,48 @@
+// Copyright 2016 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';
+
+/// A class to maintain a list of items, fire events when items are added or
+/// removed, and calculate a diff of changes when a new list of items is
+/// available.
+class ItemListNotifier<T> {
+ ItemListNotifier() {
+ _items = new Set<T>();
+ }
+
+ ItemListNotifier.from(List<T> items) {
+ _items = new Set<T>.from(items);
+ }
+
+ Set<T> _items;
+
+ StreamController<T> _addedController = new StreamController<T>.broadcast();
+ StreamController<T> _removedController = new StreamController<T>.broadcast();
+
+ Stream<T> get onAdded => _addedController.stream;
+ Stream<T> get onRemoved => _removedController.stream;
+
+ List<T> get items => _items.toList();
+
+ void updateWithNewList(List<T> updatedList) {
+ Set<T> updatedSet = new Set<T>.from(updatedList);
+
+ Set<T> addedItems = updatedSet.difference(_items);
+ Set<T> removedItems = _items.difference(updatedSet);
+
+ _items = updatedSet;
+
+ for (T item in addedItems)
+ _addedController.add(item);
+ for (T item in removedItems)
+ _removedController.add(item);
+ }
+
+ /// Close the streams.
+ void dispose() {
+ _addedController.close();
+ _removedController.close();
+ }
+}
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index 252e929..beea6cb 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -6,15 +6,13 @@
import 'dart:convert';
import 'dart:io';
-import '../android/adb.dart';
-import '../android/android_sdk.dart';
-import '../android/device_android.dart';
+import '../android/android_device.dart';
import '../base/context.dart';
import '../base/logger.dart';
import '../device.dart';
import '../globals.dart';
-import '../ios/device_ios.dart';
-import '../ios/simulator.dart';
+import '../ios/devices.dart';
+import '../ios/simulators.dart';
import '../runner/flutter_command.dart';
import 'run.dart';
import 'stop.dart' as stop;
@@ -126,10 +124,8 @@
throw 'no domain for method: $method';
_domainMap[prefix].handleCommand(name, id, request['params']);
- } catch (error, trace) {
+ } catch (error) {
_send({'id': id, 'error': _toJsonable(error)});
- stderr.writeln('error handling $request: $error');
- stderr.writeln(trace);
}
}
@@ -170,8 +166,6 @@
}
}).catchError((error, trace) {
_send({'id': id, 'error': _toJsonable(error)});
- stderr.writeln("error handling '$name.$command': $error");
- stderr.writeln(trace);
});
}
@@ -286,166 +280,65 @@
/// This domain lets callers list and monitor connected devices.
///
-/// It exports a `getDevices()` call, as well as firing `device.added`,
-/// `device.removed`, and `device.changed` events.
+/// It exports a `getDevices()` call, as well as firing `device.added` and
+/// `device.removed` events.
class DeviceDomain extends Domain {
DeviceDomain(Daemon daemon) : super(daemon, 'device') {
registerHandler('getDevices', getDevices);
+ registerHandler('enable', enable);
+ registerHandler('disable', disable);
- _androidDeviceDiscovery = new AndroidDeviceDiscovery();
- _androidDeviceDiscovery.onAdded.listen((Device device) {
- sendEvent('device.added', _deviceToMap(device));
- });
- _androidDeviceDiscovery.onRemoved.listen((Device device) {
- sendEvent('device.removed', _deviceToMap(device));
- });
- _androidDeviceDiscovery.onChanged.listen((Device device) {
- sendEvent('device.changed', _deviceToMap(device));
- });
+ PollingDeviceDiscovery deviceDiscovery = new AndroidDevices();
+ if (deviceDiscovery.supportsPlatform)
+ _discoverers.add(deviceDiscovery);
- if (Platform.isMacOS) {
- _iosSimulatorDeviceDiscovery = new IOSSimulatorDeviceDiscovery();
- _iosSimulatorDeviceDiscovery.onAdded.listen((Device device) {
+ deviceDiscovery = new IOSDevices();
+ if (deviceDiscovery.supportsPlatform)
+ _discoverers.add(deviceDiscovery);
+
+ deviceDiscovery = new IOSSimulators();
+ if (deviceDiscovery.supportsPlatform)
+ _discoverers.add(deviceDiscovery);
+
+ for (PollingDeviceDiscovery discoverer in _discoverers) {
+ discoverer.onAdded.listen((Device device) {
sendEvent('device.added', _deviceToMap(device));
});
- _iosSimulatorDeviceDiscovery.onRemoved.listen((Device device) {
+ discoverer.onRemoved.listen((Device device) {
sendEvent('device.removed', _deviceToMap(device));
});
}
}
- AndroidDeviceDiscovery _androidDeviceDiscovery;
- IOSSimulatorDeviceDiscovery _iosSimulatorDeviceDiscovery;
+ List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
Future<List<Device>> getDevices(dynamic args) {
- List<Device> devices = <Device>[];
- devices.addAll(_androidDeviceDiscovery.getDevices());
- if (_iosSimulatorDeviceDiscovery != null)
- devices.addAll(_iosSimulatorDeviceDiscovery.getDevices());
+ List<Device> devices = _discoverers.expand((PollingDeviceDiscovery discoverer) {
+ return discoverer.devices;
+ }).toList();
return new Future.value(devices);
}
- void dispose() {
- _androidDeviceDiscovery.dispose();
- _iosSimulatorDeviceDiscovery?.dispose();
- }
-}
-
-class AndroidDeviceDiscovery {
- AndroidDeviceDiscovery() {
- _initAdb();
-
- if (_adb != null) {
- _subscription = _adb.trackDevices().listen(_handleUpdatedDevices);
+ /// Enable device events.
+ Future enable(dynamic args) {
+ for (PollingDeviceDiscovery discoverer in _discoverers) {
+ discoverer.startPolling();
}
+ return new Future.value();
}
- Adb _adb;
- StreamSubscription _subscription;
- Map<String, AndroidDevice> _devices = new Map<String, AndroidDevice>();
-
- StreamController<Device> addedController = new StreamController<Device>.broadcast();
- StreamController<Device> removedController = new StreamController<Device>.broadcast();
- StreamController<Device> changedController = new StreamController<Device>.broadcast();
-
- List<Device> getDevices() => _devices.values.toList();
-
- Stream<Device> get onAdded => addedController.stream;
- Stream<Device> get onRemoved => removedController.stream;
- Stream<Device> get onChanged => changedController.stream;
-
- void _initAdb() {
- if (_adb == null) {
- _adb = new Adb(getAdbPath(androidSdk));
- if (!_adb.exists())
- _adb = null;
+ /// Disable device events.
+ Future disable(dynamic args) {
+ for (PollingDeviceDiscovery discoverer in _discoverers) {
+ discoverer.stopPolling();
}
- }
-
- void _handleUpdatedDevices(List<AdbDevice> newDevices) {
- List<AndroidDevice> currentDevices = new List.from(getDevices());
-
- for (AdbDevice device in newDevices) {
- AndroidDevice androidDevice = _devices[device.id];
-
- if (androidDevice == null) {
- // device added
- androidDevice = new AndroidDevice(
- device.id,
- productID: device.productID,
- modelID: device.modelID,
- deviceCodeName: device.deviceCodeName,
- connected: device.isAvailable
- );
- _devices[androidDevice.id] = androidDevice;
- addedController.add(androidDevice);
- } else {
- currentDevices.remove(androidDevice);
-
- // check state
- if (androidDevice.isConnected() != device.isAvailable) {
- androidDevice.setConnected(device.isAvailable);
- changedController.add(androidDevice);
- }
- }
- }
-
- // device removed
- for (AndroidDevice device in currentDevices) {
- _devices.remove(device.id);
-
- removedController.add(device);
- }
+ return new Future.value();
}
void dispose() {
- _subscription?.cancel();
- }
-}
-
-class IOSSimulatorDeviceDiscovery {
- IOSSimulatorDeviceDiscovery() {
- _subscription = SimControl.trackDevices().listen(_handleUpdatedDevices);
- }
-
- StreamSubscription<List<SimDevice>> _subscription;
-
- Map<String, IOSSimulator> _devices = new Map<String, IOSSimulator>();
-
- StreamController<Device> addedController = new StreamController<Device>.broadcast();
- StreamController<Device> removedController = new StreamController<Device>.broadcast();
-
- List<Device> getDevices() => _devices.values.toList();
-
- Stream<Device> get onAdded => addedController.stream;
- Stream<Device> get onRemoved => removedController.stream;
-
- void _handleUpdatedDevices(List<SimDevice> newDevices) {
- List<IOSSimulator> currentDevices = new List.from(getDevices());
-
- for (SimDevice device in newDevices) {
- IOSSimulator androidDevice = _devices[device.udid];
-
- if (androidDevice == null) {
- // device added
- androidDevice = new IOSSimulator(device.udid, name: device.name);
- _devices[androidDevice.id] = androidDevice;
- addedController.add(androidDevice);
- } else {
- currentDevices.remove(androidDevice);
- }
+ for (PollingDeviceDiscovery discoverer in _discoverers) {
+ discoverer.dispose();
}
-
- // device removed
- for (IOSSimulator device in currentDevices) {
- _devices.remove(device.id);
-
- removedController.add(device);
- }
- }
-
- void dispose() {
- _subscription?.cancel();
}
}
@@ -490,7 +383,7 @@
}
void printTrace(String message) {
- _messageController.add(new LogMessage('trace', message));
+ // This is a lot of traffic to send over the wire.
}
}
diff --git a/packages/flutter_tools/lib/src/commands/install.dart b/packages/flutter_tools/lib/src/commands/install.dart
index b9f8684..8e17360 100644
--- a/packages/flutter_tools/lib/src/commands/install.dart
+++ b/packages/flutter_tools/lib/src/commands/install.dart
@@ -7,7 +7,7 @@
import '../application_package.dart';
import '../device.dart';
-import '../ios/simulator.dart';
+import '../ios/simulators.dart';
import '../runner/flutter_command.dart';
class InstallCommand extends FlutterCommand {
diff --git a/packages/flutter_tools/lib/src/commands/trace.dart b/packages/flutter_tools/lib/src/commands/trace.dart
index a5b0744..f8f7a43 100644
--- a/packages/flutter_tools/lib/src/commands/trace.dart
+++ b/packages/flutter_tools/lib/src/commands/trace.dart
@@ -4,7 +4,7 @@
import 'dart:async';
-import '../android/device_android.dart';
+import '../android/android_device.dart';
import '../application_package.dart';
import '../globals.dart';
import '../runner/flutter_command.dart';
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 04562ad..df00d2b 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -4,12 +4,14 @@
import 'dart:async';
-import 'android/device_android.dart';
+import 'android/android_device.dart';
import 'application_package.dart';
import 'base/common.dart';
+import 'base/utils.dart';
import 'build_configuration.dart';
import 'globals.dart';
-import 'ios/device_ios.dart';
+import 'ios/devices.dart';
+import 'ios/simulators.dart';
import 'toolchain.dart';
/// A class to get all available devices.
@@ -18,27 +20,9 @@
/// of their methods are invoked.
DeviceManager() {
// Register the known discoverers.
- _deviceDiscoverers.add(new AndroidDeviceDiscovery());
- _deviceDiscoverers.add(new IOSDeviceDiscovery());
- _deviceDiscoverers.add(new IOSSimulatorDiscovery());
- }
-
- Future _init() {
- if (_initedCompleter == null) {
- _initedCompleter = new Completer();
-
- Future.forEach(_deviceDiscoverers, (DeviceDiscovery discoverer) {
- if (!discoverer.supportsPlatform)
- return null;
- return discoverer.init();
- }).then((_) {
- _initedCompleter.complete();
- }).catchError((error, stackTrace) {
- _initedCompleter.completeError(error, stackTrace);
- });
- }
-
- return _initedCompleter.future;
+ _deviceDiscoverers.add(new AndroidDevices());
+ _deviceDiscoverers.add(new IOSDevices());
+ _deviceDiscoverers.add(new IOSSimulators());
}
List<DeviceDiscovery> _deviceDiscoverers = <DeviceDiscovery>[];
@@ -46,8 +30,6 @@
/// A user-specified device ID.
String specifiedDeviceId;
- Completer _initedCompleter;
-
bool get hasSpecifiedDeviceId => specifiedDeviceId != null;
/// Return the device with the matching ID; else, complete the Future with
@@ -75,8 +57,6 @@
/// Return the list of all connected devices.
Future<List<Device>> getAllConnectedDevices() async {
- await _init();
-
return _deviceDiscoverers
.where((DeviceDiscovery discoverer) => discoverer.supportsPlatform)
.expand((DeviceDiscovery discoverer) => discoverer.devices)
@@ -87,10 +67,60 @@
/// An abstract class to discover and enumerate a specific type of devices.
abstract class DeviceDiscovery {
bool get supportsPlatform;
- Future init();
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;
+ }
+
+ 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();
+
+ String toString() => '$name device discovery';
+}
+
abstract class Device {
Device(this.id);
@@ -139,6 +169,16 @@
/// Stop an app package on the current device.
Future<bool> stopApp(ApplicationPackage app);
+ int get hashCode => id.hashCode;
+
+ bool operator ==(dynamic other) {
+ if (identical(this, other))
+ return true;
+ if (other is! Device)
+ return false;
+ return id == other.id;
+ }
+
String toString() => '$runtimeType $id';
}
diff --git a/packages/flutter_tools/lib/src/ios/device_ios.dart b/packages/flutter_tools/lib/src/ios/device_ios.dart
deleted file mode 100644
index 10bb965..0000000
--- a/packages/flutter_tools/lib/src/ios/device_ios.dart
+++ /dev/null
@@ -1,712 +0,0 @@
-// Copyright 2016 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:convert';
-import 'dart:io';
-
-import 'package:path/path.dart' as path;
-
-import '../application_package.dart';
-import '../artifacts.dart';
-import '../base/common.dart';
-import '../base/process.dart';
-import '../build_configuration.dart';
-import '../device.dart';
-import '../globals.dart';
-import '../ios/setup_xcodeproj.dart';
-import '../services.dart';
-import '../toolchain.dart';
-import 'simulator.dart';
-
-const String _ideviceinstallerInstructions =
- 'To work with iOS devices, please install ideviceinstaller.\n'
- 'If you use homebrew, you can install it with "\$ brew install ideviceinstaller".';
-
-class IOSDeviceDiscovery extends DeviceDiscovery {
- List<Device> _devices = <Device>[];
-
- bool get supportsPlatform => Platform.isMacOS;
-
- Future init() {
- _devices = IOSDevice.getAttachedDevices();
- return new Future.value();
- }
-
- List<Device> get devices => _devices;
-}
-
-class IOSSimulatorDiscovery extends DeviceDiscovery {
- List<Device> _devices = <Device>[];
-
- bool get supportsPlatform => Platform.isMacOS;
-
- Future init() {
- _devices = IOSSimulator.getAttachedDevices();
- return new Future.value();
- }
-
- List<Device> get devices => _devices;
-}
-
-class IOSDevice extends Device {
- IOSDevice(String id, { this.name }) : super(id) {
- _installerPath = _checkForCommand('ideviceinstaller');
- _listerPath = _checkForCommand('idevice_id');
- _informerPath = _checkForCommand('ideviceinfo');
- _debuggerPath = _checkForCommand('idevicedebug');
- _loggerPath = _checkForCommand('idevicesyslog');
- _pusherPath = _checkForCommand(
- 'ios-deploy',
- 'To copy files to iOS devices, please install ios-deploy. '
- 'You can do this using homebrew as follows:\n'
- '\$ brew tap flutter/flutter\n'
- '\$ brew install ios-deploy');
- }
-
- String _installerPath;
- String get installerPath => _installerPath;
-
- String _listerPath;
- String get listerPath => _listerPath;
-
- String _informerPath;
- String get informerPath => _informerPath;
-
- String _debuggerPath;
- String get debuggerPath => _debuggerPath;
-
- String _loggerPath;
- String get loggerPath => _loggerPath;
-
- String _pusherPath;
- String get pusherPath => _pusherPath;
-
- final String name;
-
- bool get supportsStartPaused => false;
-
- static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
- if (!doctor.iosWorkflow.hasIdeviceId)
- return <IOSDevice>[];
-
- List<IOSDevice> devices = [];
- for (String id in _getAttachedDeviceIDs(mockIOS)) {
- String name = _getDeviceName(id, mockIOS);
- devices.add(new IOSDevice(id, name: name));
- }
- return devices;
- }
-
- static Iterable<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) {
- String listerPath = (mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id');
- try {
- String output = runSync([listerPath, '-l']);
- return output.trim().split('\n').where((String s) => s != null && s.isNotEmpty);
- } catch (e) {
- return <String>[];
- }
- }
-
- static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) {
- String informerPath = (mockIOS != null)
- ? mockIOS.informerPath
- : _checkForCommand('ideviceinfo');
- return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]).trim();
- }
-
- static final Map<String, String> _commandMap = {};
- static String _checkForCommand(
- String command, [
- String macInstructions = _ideviceinstallerInstructions
- ]) {
- return _commandMap.putIfAbsent(command, () {
- try {
- command = runCheckedSync(['which', command]).trim();
- } catch (e) {
- if (Platform.isMacOS) {
- printError('$command not found. $macInstructions');
- } else {
- printError('Cannot control iOS devices or simulators. $command is not available on your platform.');
- }
- }
- return command;
- });
- }
-
- @override
- bool installApp(ApplicationPackage app) {
- try {
- runCheckedSync([installerPath, '-i', app.localPath]);
- return true;
- } catch (e) {
- return false;
- }
- return false;
- }
-
- @override
- bool isConnected() => _getAttachedDeviceIDs().contains(id);
-
- @override
- bool isSupported() => true;
-
- @override
- bool isAppInstalled(ApplicationPackage app) {
- try {
- String apps = runCheckedSync([installerPath, '--list-apps']);
- if (new RegExp(app.id, multiLine: true).hasMatch(apps)) {
- return true;
- }
- } catch (e) {
- return false;
- }
- return false;
- }
-
- @override
- Future<bool> startApp(
- ApplicationPackage app,
- Toolchain toolchain, {
- String mainPath,
- String route,
- bool checked: true,
- bool clearLogs: false,
- bool startPaused: false,
- int debugPort: observatoryDefaultPort,
- Map<String, dynamic> platformArgs
- }) async {
- // TODO(chinmaygarde): Use checked, mainPath, route, clearLogs.
- // TODO(devoncarew): Handle startPaused, debugPort.
- printTrace('Building ${app.name} for $id');
-
- // Step 1: Install the precompiled application if necessary.
- bool buildResult = await _buildIOSXcodeProject(app, buildForDevice: true);
- if (!buildResult) {
- printError('Could not build the precompiled application for the device.');
- return false;
- }
-
- // Step 2: Check that the application exists at the specified path.
- Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphoneos', 'Runner.app'));
- bool bundleExists = bundle.existsSync();
- if (!bundleExists) {
- printError('Could not find the built application bundle at ${bundle.path}.');
- return false;
- }
-
- // Step 3: Attempt to install the application on the device.
- int installationResult = await runCommandAndStreamOutput([
- '/usr/bin/env',
- 'ios-deploy',
- '--id',
- id,
- '--bundle',
- bundle.path,
- ]);
-
- if (installationResult != 0) {
- printError('Could not install ${bundle.path} on $id.');
- return false;
- }
-
- printTrace('Installation successful.');
- return true;
- }
-
- @override
- Future<bool> stopApp(ApplicationPackage app) async {
- // Currently we don't have a way to stop an app running on iOS.
- return false;
- }
-
- Future<bool> pushFile(ApplicationPackage app, String localFile, String targetFile) async {
- if (Platform.isMacOS) {
- runSync(<String>[
- pusherPath,
- '-t',
- '1',
- '--bundle_id',
- app.id,
- '--upload',
- localFile,
- '--to',
- targetFile
- ]);
- return true;
- } else {
- return false;
- }
- return false;
- }
-
- @override
- TargetPlatform get platform => TargetPlatform.iOS;
-
- DeviceLogReader createLogReader() => new _IOSDeviceLogReader(this);
-}
-
-class IOSSimulator extends Device {
- IOSSimulator(String id, { this.name }) : super(id);
-
- static List<IOSSimulator> getAttachedDevices() {
- if (!xcode.isInstalledAndMeetsVersionCheck)
- return <IOSSimulator>[];
-
- return SimControl.getConnectedDevices().map((SimDevice device) {
- return new IOSSimulator(device.udid, name: device.name);
- }).toList();
- }
-
- final String name;
-
- String get xcrunPath => path.join('/usr', 'bin', 'xcrun');
-
- String _getSimulatorPath() {
- return path.join(_homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', id);
- }
-
- String _getSimulatorAppHomeDirectory(ApplicationPackage app) {
- String simulatorPath = _getSimulatorPath();
- if (simulatorPath == null)
- return null;
- return path.join(simulatorPath, 'data');
- }
-
- @override
- bool installApp(ApplicationPackage app) {
- if (!isConnected())
- return false;
-
- try {
- SimControl.install(id, app.localPath);
- return true;
- } catch (e) {
- return false;
- }
- }
-
- @override
- bool isConnected() {
- if (!Platform.isMacOS)
- return false;
- return SimControl.getConnectedDevices().any((SimDevice device) => device.udid == id);
- }
-
- @override
- bool isSupported() {
- if (!Platform.isMacOS) {
- _supportMessage = "Not supported on a non Mac host";
- return false;
- }
-
- // Step 1: Check if the device is part of a blacklisted category.
- // We do not support WatchOS or tvOS devices.
-
- RegExp blacklist = new RegExp(r'Apple (TV|Watch)', caseSensitive: false);
-
- if (blacklist.hasMatch(name)) {
- _supportMessage = "Flutter does not support either the Apple TV or Watch. Choose an iPhone 5s or above.";
- return false;
- }
-
- // Step 2: Check if the device must be rejected because of its version.
- // There is an artitifical check on older simulators where arm64
- // targetted applications cannot be run (even though the
- // Flutter runner on the simulator is completely different).
-
- RegExp versionExp = new RegExp(r'iPhone ([0-9])+');
- Match match = versionExp.firstMatch(name);
-
- if (match == null) {
- // Not an iPhone. All available non-iPhone simulators are compatible.
- return true;
- }
-
- if (int.parse(match.group(1)) > 5) {
- // iPhones 6 and above are always fine.
- return true;
- }
-
- // The 's' subtype of 5 is compatible.
- if (name.contains('iPhone 5s')) {
- return true;
- }
-
- _supportMessage = "The simulator version is too old. Choose an iPhone 5s or above.";
- return false;
- }
-
- String _supportMessage;
-
- @override
- String supportMessage() {
- if (isSupported()) {
- return "Supported";
- }
-
- return _supportMessage != null ? _supportMessage : "Unknown";
- }
-
- @override
- bool isAppInstalled(ApplicationPackage app) {
- try {
- String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
- return FileSystemEntity.isDirectorySync(simulatorHomeDirectory);
- } catch (e) {
- return false;
- }
- }
-
- @override
- Future<bool> startApp(
- ApplicationPackage app,
- Toolchain toolchain, {
- String mainPath,
- String route,
- bool checked: true,
- bool clearLogs: false,
- bool startPaused: false,
- int debugPort: observatoryDefaultPort,
- Map<String, dynamic> platformArgs
- }) async {
- // TODO(chinmaygarde): Use mainPath, route.
- printTrace('Building ${app.name} for $id.');
-
- if (clearLogs)
- this.clearLogs();
-
- // Step 1: Build the Xcode project.
- bool buildResult = await _buildIOSXcodeProject(app, buildForDevice: false);
- if (!buildResult) {
- printError('Could not build the application for the simulator.');
- return false;
- }
-
- // Step 2: Assert that the Xcode project was successfully built.
- Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphonesimulator', 'Runner.app'));
- bool bundleExists = await bundle.exists();
- if (!bundleExists) {
- printError('Could not find the built application bundle at ${bundle.path}.');
- return false;
- }
-
- // Step 3: Install the updated bundle to the simulator.
- SimControl.install(id, path.absolute(bundle.path));
-
- // Step 4: Prepare launch arguments.
- List<String> args = <String>[];
-
- if (checked)
- args.add("--enable-checked-mode");
-
- if (startPaused)
- args.add("--start-paused");
-
- if (debugPort != observatoryDefaultPort)
- args.add("--observatory-port=$debugPort");
-
- // Step 5: Launch the updated application in the simulator.
- try {
- SimControl.launch(id, app.id, args);
- } catch (error) {
- printError('$error');
- return false;
- }
-
- printTrace('Successfully started ${app.name} on $id.');
-
- return true;
- }
-
- @override
- Future<bool> stopApp(ApplicationPackage app) async {
- // Currently we don't have a way to stop an app running on iOS.
- return false;
- }
-
- Future<bool> pushFile(
- ApplicationPackage app, String localFile, String targetFile) async {
- if (Platform.isMacOS) {
- String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
- runCheckedSync(<String>['cp', localFile, path.join(simulatorHomeDirectory, targetFile)]);
- return true;
- }
- return false;
- }
-
- String get logFilePath {
- return path.join(_homeDirectory, 'Library', 'Logs', 'CoreSimulator', id, 'system.log');
- }
-
- @override
- TargetPlatform get platform => TargetPlatform.iOSSimulator;
-
- DeviceLogReader createLogReader() => new _IOSSimulatorLogReader(this);
-
- void clearLogs() {
- File logFile = new File(logFilePath);
- if (logFile.existsSync()) {
- RandomAccessFile randomFile = logFile.openSync(mode: FileMode.WRITE);
- randomFile.truncateSync(0);
- randomFile.closeSync();
- }
- }
-
- void ensureLogsExists() {
- File logFile = new File(logFilePath);
- if (!logFile.existsSync())
- logFile.writeAsBytesSync(<int>[]);
- }
-}
-
-class _IOSDeviceLogReader extends DeviceLogReader {
- _IOSDeviceLogReader(this.device);
-
- final IOSDevice device;
-
- String get name => device.name;
-
- // TODO(devoncarew): Support [clear].
- Future<int> logs({ bool clear: false }) async {
- if (!device.isConnected())
- return 2;
-
- return await runCommandAndStreamOutput(
- <String>[device.loggerPath],
- prefix: '[$name] ',
- filter: new RegExp(r'Runner')
- );
- }
-
- int get hashCode => name.hashCode;
-
- bool operator ==(dynamic other) {
- if (identical(this, other))
- return true;
- if (other is! _IOSDeviceLogReader)
- return false;
- return other.name == name;
- }
-}
-
-class _IOSSimulatorLogReader extends DeviceLogReader {
- _IOSSimulatorLogReader(this.device);
-
- final IOSSimulator device;
-
- bool _lastWasFiltered = false;
-
- String get name => device.name;
-
- Future<int> logs({ bool clear: false }) async {
- if (!device.isConnected())
- return 2;
-
- if (clear)
- device.clearLogs();
-
- device.ensureLogsExists();
-
- // Match the log prefix (in order to shorten it):
- // 'Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: ...'
- RegExp mapRegex = new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$');
- // Jan 31 19:23:28 --- last message repeated 1 time ---
- RegExp lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ --- (.*) ---$');
-
- // This filter matches many Flutter lines in the log:
- // new RegExp(r'(FlutterRunner|flutter.runner.Runner|$id)'), but it misses
- // a fair number, including ones that would be useful in diagnosing crashes.
- // For now, we're not filtering the log file (but do clear it with each run).
-
- Future<int> result = runCommandAndStreamOutput(
- <String>['tail', '-n', '+0', '-F', device.logFilePath],
- prefix: '[$name] ',
- mapFunction: (String string) {
- Match match = mapRegex.matchAsPrefix(string);
- if (match != null) {
- _lastWasFiltered = true;
-
- // Filter out some messages that clearly aren't related to Flutter.
- if (string.contains(': could not find icon for representation -> com.apple.'))
- return null;
- String category = match.group(1);
- String content = match.group(2);
- if (category == 'Game Center' || category == 'itunesstored' || category == 'nanoregistrylaunchd' ||
- category == 'mstreamd' || category == 'syncdefaultsd' || category == 'companionappd' ||
- category == 'searchd')
- return null;
-
- _lastWasFiltered = false;
-
- if (category == 'Runner')
- return content;
- return '$category: $content';
- }
- match = lastMessageRegex.matchAsPrefix(string);
- if (match != null && !_lastWasFiltered)
- return '(${match.group(1)})';
- return string;
- }
- );
-
- // Track system.log crashes.
- // ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
- runCommandAndStreamOutput(
- <String>['tail', '-F', '/private/var/log/system.log'],
- prefix: '[$name] ',
- filter: new RegExp(r' FlutterRunner\[\d+\] '),
- mapFunction: (String string) {
- Match match = mapRegex.matchAsPrefix(string);
- return match == null ? string : '${match.group(1)}: ${match.group(2)}';
- }
- );
-
- return await result;
- }
-
- int get hashCode => device.logFilePath.hashCode;
-
- bool operator ==(dynamic other) {
- if (identical(this, other))
- return true;
- if (other is! _IOSSimulatorLogReader)
- return false;
- return other.device.logFilePath == device.logFilePath;
- }
-}
-
-final RegExp _xcodeVersionRegExp = new RegExp(r'Xcode (\d+)\..*');
-final String _xcodeRequirement = 'Xcode 7.0 or greater is required to develop for iOS.';
-
-String get _homeDirectory => path.absolute(Platform.environment['HOME']);
-
-bool _checkXcodeVersion() {
- if (!Platform.isMacOS)
- return false;
- try {
- String version = runCheckedSync(<String>['xcodebuild', '-version']);
- Match match = _xcodeVersionRegExp.firstMatch(version);
- if (int.parse(match[1]) < 7) {
- printError('Found "${match[0]}". $_xcodeRequirement');
- return false;
- }
- } catch (e) {
- printError('Cannot find "xcodebuid". $_xcodeRequirement');
- return false;
- }
- return true;
-}
-
-bool _validateEngineRevision(ApplicationPackage app) {
- String skyRevision = ArtifactStore.engineRevision;
- String iosRevision = _getIOSEngineRevision(app);
-
- if (iosRevision != skyRevision) {
- printError("Error: incompatible sky_engine revision.");
- printStatus('sky_engine revision: $skyRevision, iOS engine revision: $iosRevision');
- return false;
- } else {
- printTrace('sky_engine revision: $skyRevision, iOS engine revision: $iosRevision');
- return true;
- }
-}
-
-String _getIOSEngineRevision(ApplicationPackage app) {
- File revisionFile = new File(path.join(app.localPath, 'REVISION'));
- if (revisionFile.existsSync()) {
- return revisionFile.readAsStringSync().trim();
- } else {
- return null;
- }
-}
-
-Future<bool> _buildIOSXcodeProject(ApplicationPackage app, { bool buildForDevice }) async {
- String flutterProjectPath = Directory.current.path;
-
- if (xcodeProjectRequiresUpdate()) {
- printTrace('Initializing the Xcode project.');
- if ((await setupXcodeProjectHarness(flutterProjectPath)) != 0) {
- printError('Could not initialize the Xcode project.');
- return false;
- }
- } else {
- updateXcodeLocalProperties(flutterProjectPath);
- }
-
- if (!_validateEngineRevision(app))
- return false;
-
- if (!_checkXcodeVersion())
- return false;
-
- // Before the build, all service definitions must be updated and the dylibs
- // copied over to a location that is suitable for Xcodebuild to find them.
-
- await _addServicesToBundle(new Directory(app.localPath));
-
- List<String> commands = <String>[
- '/usr/bin/env', 'xcrun', 'xcodebuild', '-target', 'Runner', '-configuration', 'Release'
- ];
-
- if (buildForDevice) {
- commands.addAll(<String>['-sdk', 'iphoneos', '-arch', 'arm64']);
- } else {
- commands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
- }
-
- try {
- runCheckedSync(commands, workingDirectory: app.localPath);
- return true;
- } catch (error) {
- return false;
- }
-}
-
-Future _addServicesToBundle(Directory bundle) async {
- List<Map<String, String>> services = [];
- printTrace("Trying to resolve native pub services.");
-
- // Step 1: Parse the service configuration yaml files present in the service
- // pub packages.
- await parseServiceConfigs(services);
- printTrace("Found ${services.length} service definition(s).");
-
- // Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up.
- Directory frameworksDirectory = new Directory(path.join(bundle.path, "Frameworks"));
- await _copyServiceFrameworks(services, frameworksDirectory);
-
- // Step 3: Copy the service definitions manifest at the correct spot for
- // xcodebuild to pick up.
- File manifestFile = new File(path.join(bundle.path, "ServiceDefinitions.json"));
- _copyServiceDefinitionsManifest(services, manifestFile);
-}
-
-Future _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async {
- printTrace("Copying service frameworks to '${path.absolute(frameworksDirectory.path)}'.");
- frameworksDirectory.createSync(recursive: true);
- for (Map<String, String> service in services) {
- String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']);
- File dylib = new File(dylibPath);
- printTrace("Copying ${dylib.path} into bundle.");
- if (!dylib.existsSync()) {
- printError("The service dylib '${dylib.path}' does not exist.");
- continue;
- }
- // Shell out so permissions on the dylib are preserved.
- runCheckedSync(['/bin/cp', dylib.path, frameworksDirectory.path]);
- }
-}
-
-void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) {
- printTrace("Creating service definitions manifest at '${manifest.path}'");
- List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => {
- 'name': service['name'],
- // Since we have already moved it to the Frameworks directory. Strip away
- // the directory and basenames.
- 'framework': path.basenameWithoutExtension(service['ios-framework'])
- }).toList();
- Map<String, dynamic> json = { 'services' : jsonServices };
- manifest.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true);
-}
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
new file mode 100644
index 0000000..b5900f0
--- /dev/null
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -0,0 +1,255 @@
+// Copyright 2016 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:io';
+
+import 'package:path/path.dart' as path;
+
+import '../application_package.dart';
+import '../base/common.dart';
+import '../base/process.dart';
+import '../build_configuration.dart';
+import '../device.dart';
+import '../globals.dart';
+import '../toolchain.dart';
+import 'mac.dart';
+
+const String _ideviceinstallerInstructions =
+ 'To work with iOS devices, please install ideviceinstaller.\n'
+ 'If you use homebrew, you can install it with "\$ brew install ideviceinstaller".';
+
+class IOSDevices extends PollingDeviceDiscovery {
+ IOSDevices() : super('IOSDevices');
+
+ bool get supportsPlatform => Platform.isMacOS;
+ List<Device> pollingGetDevices() => IOSDevice.getAttachedDevices();
+}
+
+class IOSDevice extends Device {
+ IOSDevice(String id, { this.name }) : super(id) {
+ _installerPath = _checkForCommand('ideviceinstaller');
+ _listerPath = _checkForCommand('idevice_id');
+ _informerPath = _checkForCommand('ideviceinfo');
+ _debuggerPath = _checkForCommand('idevicedebug');
+ _loggerPath = _checkForCommand('idevicesyslog');
+ _pusherPath = _checkForCommand(
+ 'ios-deploy',
+ 'To copy files to iOS devices, please install ios-deploy. '
+ 'You can do this using homebrew as follows:\n'
+ '\$ brew tap flutter/flutter\n'
+ '\$ brew install ios-deploy');
+ }
+
+ String _installerPath;
+ String get installerPath => _installerPath;
+
+ String _listerPath;
+ String get listerPath => _listerPath;
+
+ String _informerPath;
+ String get informerPath => _informerPath;
+
+ String _debuggerPath;
+ String get debuggerPath => _debuggerPath;
+
+ String _loggerPath;
+ String get loggerPath => _loggerPath;
+
+ String _pusherPath;
+ String get pusherPath => _pusherPath;
+
+ final String name;
+
+ bool get supportsStartPaused => false;
+
+ static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
+ if (!doctor.iosWorkflow.hasIdeviceId)
+ return <IOSDevice>[];
+
+ List<IOSDevice> devices = [];
+ for (String id in _getAttachedDeviceIDs(mockIOS)) {
+ String name = _getDeviceName(id, mockIOS);
+ devices.add(new IOSDevice(id, name: name));
+ }
+ return devices;
+ }
+
+ static Iterable<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) {
+ String listerPath = (mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id');
+ try {
+ String output = runSync([listerPath, '-l']);
+ return output.trim().split('\n').where((String s) => s != null && s.isNotEmpty);
+ } catch (e) {
+ return <String>[];
+ }
+ }
+
+ static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) {
+ String informerPath = (mockIOS != null)
+ ? mockIOS.informerPath
+ : _checkForCommand('ideviceinfo');
+ return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]).trim();
+ }
+
+ static final Map<String, String> _commandMap = {};
+ static String _checkForCommand(
+ String command, [
+ String macInstructions = _ideviceinstallerInstructions
+ ]) {
+ return _commandMap.putIfAbsent(command, () {
+ try {
+ command = runCheckedSync(['which', command]).trim();
+ } catch (e) {
+ if (Platform.isMacOS) {
+ printError('$command not found. $macInstructions');
+ } else {
+ printError('Cannot control iOS devices or simulators. $command is not available on your platform.');
+ }
+ }
+ return command;
+ });
+ }
+
+ @override
+ bool installApp(ApplicationPackage app) {
+ try {
+ runCheckedSync([installerPath, '-i', app.localPath]);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ return false;
+ }
+
+ @override
+ bool isConnected() => _getAttachedDeviceIDs().contains(id);
+
+ @override
+ bool isSupported() => true;
+
+ @override
+ bool isAppInstalled(ApplicationPackage app) {
+ try {
+ String apps = runCheckedSync([installerPath, '--list-apps']);
+ if (new RegExp(app.id, multiLine: true).hasMatch(apps)) {
+ return true;
+ }
+ } catch (e) {
+ return false;
+ }
+ return false;
+ }
+
+ @override
+ Future<bool> startApp(
+ ApplicationPackage app,
+ Toolchain toolchain, {
+ String mainPath,
+ String route,
+ bool checked: true,
+ bool clearLogs: false,
+ bool startPaused: false,
+ int debugPort: observatoryDefaultPort,
+ Map<String, dynamic> platformArgs
+ }) async {
+ // TODO(chinmaygarde): Use checked, mainPath, route, clearLogs.
+ // TODO(devoncarew): Handle startPaused, debugPort.
+ printTrace('Building ${app.name} for $id');
+
+ // Step 1: Install the precompiled application if necessary.
+ bool buildResult = await buildIOSXcodeProject(app, buildForDevice: true);
+ if (!buildResult) {
+ printError('Could not build the precompiled application for the device.');
+ return false;
+ }
+
+ // Step 2: Check that the application exists at the specified path.
+ Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphoneos', 'Runner.app'));
+ bool bundleExists = bundle.existsSync();
+ if (!bundleExists) {
+ printError('Could not find the built application bundle at ${bundle.path}.');
+ return false;
+ }
+
+ // Step 3: Attempt to install the application on the device.
+ int installationResult = await runCommandAndStreamOutput([
+ '/usr/bin/env',
+ 'ios-deploy',
+ '--id',
+ id,
+ '--bundle',
+ bundle.path,
+ ]);
+
+ if (installationResult != 0) {
+ printError('Could not install ${bundle.path} on $id.');
+ return false;
+ }
+
+ printTrace('Installation successful.');
+ return true;
+ }
+
+ @override
+ Future<bool> stopApp(ApplicationPackage app) async {
+ // Currently we don't have a way to stop an app running on iOS.
+ return false;
+ }
+
+ Future<bool> pushFile(ApplicationPackage app, String localFile, String targetFile) async {
+ if (Platform.isMacOS) {
+ runSync(<String>[
+ pusherPath,
+ '-t',
+ '1',
+ '--bundle_id',
+ app.id,
+ '--upload',
+ localFile,
+ '--to',
+ targetFile
+ ]);
+ return true;
+ } else {
+ return false;
+ }
+ return false;
+ }
+
+ @override
+ TargetPlatform get platform => TargetPlatform.iOS;
+
+ DeviceLogReader createLogReader() => new _IOSDeviceLogReader(this);
+}
+
+class _IOSDeviceLogReader extends DeviceLogReader {
+ _IOSDeviceLogReader(this.device);
+
+ final IOSDevice device;
+
+ String get name => device.name;
+
+ // TODO(devoncarew): Support [clear].
+ Future<int> logs({ bool clear: false }) async {
+ if (!device.isConnected())
+ return 2;
+
+ return await runCommandAndStreamOutput(
+ <String>[device.loggerPath],
+ prefix: '[$name] ',
+ filter: new RegExp(r'Runner')
+ );
+ }
+
+ int get hashCode => name.hashCode;
+
+ bool operator ==(dynamic other) {
+ if (identical(this, other))
+ return true;
+ if (other is! _IOSDeviceLogReader)
+ return false;
+ return other.name == name;
+ }
+}
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index d2aacc3..ef95ce0 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -2,8 +2,23 @@
// 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:convert' show JSON;
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+
+import '../application_package.dart';
+import '../artifacts.dart';
import '../base/context.dart';
import '../base/process.dart';
+import '../globals.dart';
+import '../services.dart';
+import 'setup_xcodeproj.dart';
+
+String get homeDirectory => path.absolute(Platform.environment['HOME']);
+
+// TODO(devoncarew): Refactor functionality into XCode.
const int kXcodeRequiredVersionMajor = 7;
const int kXcodeRequiredVersionMinor = 2;
@@ -51,3 +66,135 @@
return false;
}
}
+
+Future<bool> buildIOSXcodeProject(ApplicationPackage app, { bool buildForDevice }) async {
+ String flutterProjectPath = Directory.current.path;
+
+ if (xcodeProjectRequiresUpdate()) {
+ printTrace('Initializing the Xcode project.');
+ if ((await setupXcodeProjectHarness(flutterProjectPath)) != 0) {
+ printError('Could not initialize the Xcode project.');
+ return false;
+ }
+ } else {
+ updateXcodeLocalProperties(flutterProjectPath);
+ }
+
+ if (!_validateEngineRevision(app))
+ return false;
+
+ if (!_checkXcodeVersion())
+ return false;
+
+ // Before the build, all service definitions must be updated and the dylibs
+ // copied over to a location that is suitable for Xcodebuild to find them.
+
+ await _addServicesToBundle(new Directory(app.localPath));
+
+ List<String> commands = <String>[
+ '/usr/bin/env', 'xcrun', 'xcodebuild', '-target', 'Runner', '-configuration', 'Release'
+ ];
+
+ if (buildForDevice) {
+ commands.addAll(<String>['-sdk', 'iphoneos', '-arch', 'arm64']);
+ } else {
+ commands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
+ }
+
+ try {
+ runCheckedSync(commands, workingDirectory: app.localPath);
+ return true;
+ } catch (error) {
+ return false;
+ }
+}
+
+final RegExp _xcodeVersionRegExp = new RegExp(r'Xcode (\d+)\..*');
+final String _xcodeRequirement = 'Xcode 7.0 or greater is required to develop for iOS.';
+
+bool _checkXcodeVersion() {
+ if (!Platform.isMacOS)
+ return false;
+ try {
+ String version = runCheckedSync(<String>['xcodebuild', '-version']);
+ Match match = _xcodeVersionRegExp.firstMatch(version);
+ if (int.parse(match[1]) < 7) {
+ printError('Found "${match[0]}". $_xcodeRequirement');
+ return false;
+ }
+ } catch (e) {
+ printError('Cannot find "xcodebuid". $_xcodeRequirement');
+ return false;
+ }
+ return true;
+}
+
+bool _validateEngineRevision(ApplicationPackage app) {
+ String skyRevision = ArtifactStore.engineRevision;
+ String iosRevision = _getIOSEngineRevision(app);
+
+ if (iosRevision != skyRevision) {
+ printError("Error: incompatible sky_engine revision.");
+ printStatus('sky_engine revision: $skyRevision, iOS engine revision: $iosRevision');
+ return false;
+ } else {
+ printTrace('sky_engine revision: $skyRevision, iOS engine revision: $iosRevision');
+ return true;
+ }
+}
+
+String _getIOSEngineRevision(ApplicationPackage app) {
+ File revisionFile = new File(path.join(app.localPath, 'REVISION'));
+ if (revisionFile.existsSync()) {
+ return revisionFile.readAsStringSync().trim();
+ } else {
+ return null;
+ }
+}
+
+Future _addServicesToBundle(Directory bundle) async {
+ List<Map<String, String>> services = [];
+ printTrace("Trying to resolve native pub services.");
+
+ // Step 1: Parse the service configuration yaml files present in the service
+ // pub packages.
+ await parseServiceConfigs(services);
+ printTrace("Found ${services.length} service definition(s).");
+
+ // Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up.
+ Directory frameworksDirectory = new Directory(path.join(bundle.path, "Frameworks"));
+ await _copyServiceFrameworks(services, frameworksDirectory);
+
+ // Step 3: Copy the service definitions manifest at the correct spot for
+ // xcodebuild to pick up.
+ File manifestFile = new File(path.join(bundle.path, "ServiceDefinitions.json"));
+ _copyServiceDefinitionsManifest(services, manifestFile);
+}
+
+Future _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async {
+ printTrace("Copying service frameworks to '${path.absolute(frameworksDirectory.path)}'.");
+ frameworksDirectory.createSync(recursive: true);
+ for (Map<String, String> service in services) {
+ String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']);
+ File dylib = new File(dylibPath);
+ printTrace("Copying ${dylib.path} into bundle.");
+ if (!dylib.existsSync()) {
+ printError("The service dylib '${dylib.path}' does not exist.");
+ continue;
+ }
+ // Shell out so permissions on the dylib are preserved.
+ runCheckedSync(['/bin/cp', dylib.path, frameworksDirectory.path]);
+ }
+}
+
+void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) {
+ printTrace("Creating service definitions manifest at '${manifest.path}'");
+ List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => {
+ 'name': service['name'],
+ // Since we have already moved it to the Frameworks directory. Strip away
+ // the directory and basenames.
+ 'framework': path.basenameWithoutExtension(service['ios-framework'])
+ }).toList();
+ Map<String, dynamic> json = { 'services' : jsonServices };
+ manifest.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true);
+}
diff --git a/packages/flutter_tools/lib/src/ios/simulator.dart b/packages/flutter_tools/lib/src/ios/simulator.dart
deleted file mode 100644
index 3328191..0000000
--- a/packages/flutter_tools/lib/src/ios/simulator.dart
+++ /dev/null
@@ -1,173 +0,0 @@
-// Copyright 2016 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:convert' show JSON;
-import 'dart:io';
-
-import '../base/process.dart';
-import '../globals.dart';
-
-const String _xcrunPath = '/usr/bin/xcrun';
-
-const String _simulatorPath =
- '/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator';
-
-/// A wrapper around the `simctl` command line tool.
-class SimControl {
- static Future<bool> boot({String deviceId}) async {
- if (_isAnyConnected())
- return true;
-
- if (deviceId == null) {
- runDetached([_simulatorPath]);
- Future<bool> checkConnection([int attempts = 20]) async {
- if (attempts == 0) {
- printStatus('Timed out waiting for iOS Simulator to boot.');
- return false;
- }
- if (!_isAnyConnected()) {
- printStatus('Waiting for iOS Simulator to boot...');
- return await new Future.delayed(new Duration(milliseconds: 500),
- () => checkConnection(attempts - 1)
- );
- }
- return true;
- }
- return await checkConnection();
- } else {
- try {
- runCheckedSync([_xcrunPath, 'simctl', 'boot', deviceId]);
- return true;
- } catch (e) {
- printError('Unable to boot iOS Simulator $deviceId: ', e);
- return false;
- }
- }
-
- return false;
- }
-
- /// Returns a list of all available devices, both potential and connected.
- static List<SimDevice> getDevices() {
- // {
- // "devices" : {
- // "com.apple.CoreSimulator.SimRuntime.iOS-8-2" : [
- // {
- // "state" : "Shutdown",
- // "availability" : " (unavailable, runtime profile not found)",
- // "name" : "iPhone 4s",
- // "udid" : "1913014C-6DCB-485D-AC6B-7CD76D322F5B"
- // },
- // ...
-
- List<String> args = <String>['simctl', 'list', '--json', 'devices'];
- printTrace('$_xcrunPath ${args.join(' ')}');
- ProcessResult results = Process.runSync(_xcrunPath, args);
- if (results.exitCode != 0) {
- printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
- return <SimDevice>[];
- }
-
- List<SimDevice> devices = <SimDevice>[];
-
- Map<String, Map<String, dynamic>> data = JSON.decode(results.stdout);
- Map<String, dynamic> devicesSection = data['devices'];
-
- for (String deviceCategory in devicesSection.keys) {
- List<dynamic> devicesData = devicesSection[deviceCategory];
-
- for (Map<String, String> data in devicesData) {
- devices.add(new SimDevice(deviceCategory, data));
- }
- }
-
- return devices;
- }
-
- /// Returns all the connected simulator devices.
- static List<SimDevice> getConnectedDevices() {
- return getDevices().where((SimDevice device) => device.isBooted).toList();
- }
-
- static StreamController<List<SimDevice>> _trackDevicesControler;
-
- /// Listens to changes in the set of connected devices. The implementation
- /// currently uses polling. Callers should be careful to call cancel() on any
- /// stream subscription when finished.
- ///
- /// TODO(devoncarew): We could investigate using the usbmuxd protocol directly.
- static Stream<List<SimDevice>> trackDevices() {
- if (_trackDevicesControler == null) {
- Timer timer;
- Set<String> deviceIds = new Set<String>();
-
- _trackDevicesControler = new StreamController.broadcast(
- onListen: () {
- timer = new Timer.periodic(new Duration(seconds: 4), (Timer timer) {
- List<SimDevice> devices = getConnectedDevices();
-
- if (_updateDeviceIds(devices, deviceIds)) {
- _trackDevicesControler.add(devices);
- }
- });
- }, onCancel: () {
- timer?.cancel();
- deviceIds.clear();
- }
- );
- }
-
- return _trackDevicesControler.stream;
- }
-
- /// Update the cached set of device IDs and return whether there were any changes.
- static bool _updateDeviceIds(List<SimDevice> devices, Set<String> deviceIds) {
- Set<String> newIds = new Set<String>.from(devices.map((SimDevice device) => device.udid));
-
- bool changed = false;
-
- for (String id in newIds) {
- if (!deviceIds.contains(id))
- changed = true;
- }
-
- for (String id in deviceIds) {
- if (!newIds.contains(id))
- changed = true;
- }
-
- deviceIds.clear();
- deviceIds.addAll(newIds);
-
- return changed;
- }
-
- static bool _isAnyConnected() => getConnectedDevices().isNotEmpty;
-
- static void install(String deviceId, String appPath) {
- runCheckedSync([_xcrunPath, 'simctl', 'install', deviceId, appPath]);
- }
-
- static void launch(String deviceId, String appIdentifier, [List<String> launchArgs]) {
- List<String> args = [_xcrunPath, 'simctl', 'launch', deviceId, appIdentifier];
- if (launchArgs != null)
- args.addAll(launchArgs);
- runCheckedSync(args);
- }
-}
-
-class SimDevice {
- SimDevice(this.category, this.data);
-
- final String category;
- final Map<String, String> data;
-
- String get state => data['state'];
- String get availability => data['availability'];
- String get name => data['name'];
- String get udid => data['udid'];
-
- bool get isBooted => state == 'Booted';
-}
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
new file mode 100644
index 0000000..9480981
--- /dev/null
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -0,0 +1,488 @@
+// Copyright 2016 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:convert' show JSON;
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+
+import '../application_package.dart';
+import '../base/common.dart';
+import '../base/process.dart';
+import '../build_configuration.dart';
+import '../device.dart';
+import '../globals.dart';
+import '../toolchain.dart';
+import 'mac.dart';
+
+const String _xcrunPath = '/usr/bin/xcrun';
+
+const String _simulatorPath =
+ '/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator';
+
+class IOSSimulators extends PollingDeviceDiscovery {
+ IOSSimulators() : super('IOSSimulators');
+
+ bool get supportsPlatform => Platform.isMacOS;
+ List<Device> pollingGetDevices() => IOSSimulator.getAttachedDevices();
+}
+
+/// A wrapper around the `simctl` command line tool.
+class SimControl {
+ static Future<bool> boot({String deviceId}) async {
+ if (_isAnyConnected())
+ return true;
+
+ if (deviceId == null) {
+ runDetached([_simulatorPath]);
+ Future<bool> checkConnection([int attempts = 20]) async {
+ if (attempts == 0) {
+ printStatus('Timed out waiting for iOS Simulator to boot.');
+ return false;
+ }
+ if (!_isAnyConnected()) {
+ printStatus('Waiting for iOS Simulator to boot...');
+ return await new Future.delayed(new Duration(milliseconds: 500),
+ () => checkConnection(attempts - 1)
+ );
+ }
+ return true;
+ }
+ return await checkConnection();
+ } else {
+ try {
+ runCheckedSync([_xcrunPath, 'simctl', 'boot', deviceId]);
+ return true;
+ } catch (e) {
+ printError('Unable to boot iOS Simulator $deviceId: ', e);
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ /// Returns a list of all available devices, both potential and connected.
+ static List<SimDevice> getDevices() {
+ // {
+ // "devices" : {
+ // "com.apple.CoreSimulator.SimRuntime.iOS-8-2" : [
+ // {
+ // "state" : "Shutdown",
+ // "availability" : " (unavailable, runtime profile not found)",
+ // "name" : "iPhone 4s",
+ // "udid" : "1913014C-6DCB-485D-AC6B-7CD76D322F5B"
+ // },
+ // ...
+
+ List<String> args = <String>['simctl', 'list', '--json', 'devices'];
+ printTrace('$_xcrunPath ${args.join(' ')}');
+ ProcessResult results = Process.runSync(_xcrunPath, args);
+ if (results.exitCode != 0) {
+ printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
+ return <SimDevice>[];
+ }
+
+ List<SimDevice> devices = <SimDevice>[];
+
+ Map<String, Map<String, dynamic>> data = JSON.decode(results.stdout);
+ Map<String, dynamic> devicesSection = data['devices'];
+
+ for (String deviceCategory in devicesSection.keys) {
+ List<dynamic> devicesData = devicesSection[deviceCategory];
+
+ for (Map<String, String> data in devicesData) {
+ devices.add(new SimDevice(deviceCategory, data));
+ }
+ }
+
+ return devices;
+ }
+
+ /// Returns all the connected simulator devices.
+ static List<SimDevice> getConnectedDevices() {
+ return getDevices().where((SimDevice device) => device.isBooted).toList();
+ }
+
+ static StreamController<List<SimDevice>> _trackDevicesControler;
+
+ /// Listens to changes in the set of connected devices. The implementation
+ /// currently uses polling. Callers should be careful to call cancel() on any
+ /// stream subscription when finished.
+ ///
+ /// TODO(devoncarew): We could investigate using the usbmuxd protocol directly.
+ static Stream<List<SimDevice>> trackDevices() {
+ if (_trackDevicesControler == null) {
+ Timer timer;
+ Set<String> deviceIds = new Set<String>();
+
+ _trackDevicesControler = new StreamController.broadcast(
+ onListen: () {
+ timer = new Timer.periodic(new Duration(seconds: 4), (Timer timer) {
+ List<SimDevice> devices = getConnectedDevices();
+
+ if (_updateDeviceIds(devices, deviceIds)) {
+ _trackDevicesControler.add(devices);
+ }
+ });
+ }, onCancel: () {
+ timer?.cancel();
+ deviceIds.clear();
+ }
+ );
+ }
+
+ return _trackDevicesControler.stream;
+ }
+
+ /// Update the cached set of device IDs and return whether there were any changes.
+ static bool _updateDeviceIds(List<SimDevice> devices, Set<String> deviceIds) {
+ Set<String> newIds = new Set<String>.from(devices.map((SimDevice device) => device.udid));
+
+ bool changed = false;
+
+ for (String id in newIds) {
+ if (!deviceIds.contains(id))
+ changed = true;
+ }
+
+ for (String id in deviceIds) {
+ if (!newIds.contains(id))
+ changed = true;
+ }
+
+ deviceIds.clear();
+ deviceIds.addAll(newIds);
+
+ return changed;
+ }
+
+ static bool _isAnyConnected() => getConnectedDevices().isNotEmpty;
+
+ static void install(String deviceId, String appPath) {
+ runCheckedSync([_xcrunPath, 'simctl', 'install', deviceId, appPath]);
+ }
+
+ static void launch(String deviceId, String appIdentifier, [List<String> launchArgs]) {
+ List<String> args = [_xcrunPath, 'simctl', 'launch', deviceId, appIdentifier];
+ if (launchArgs != null)
+ args.addAll(launchArgs);
+ runCheckedSync(args);
+ }
+}
+
+class SimDevice {
+ SimDevice(this.category, this.data);
+
+ final String category;
+ final Map<String, String> data;
+
+ String get state => data['state'];
+ String get availability => data['availability'];
+ String get name => data['name'];
+ String get udid => data['udid'];
+
+ bool get isBooted => state == 'Booted';
+}
+
+class IOSSimulator extends Device {
+ IOSSimulator(String id, { this.name }) : super(id);
+
+ static List<IOSSimulator> getAttachedDevices() {
+ if (!xcode.isInstalledAndMeetsVersionCheck)
+ return <IOSSimulator>[];
+
+ return SimControl.getConnectedDevices().map((SimDevice device) {
+ return new IOSSimulator(device.udid, name: device.name);
+ }).toList();
+ }
+
+ final String name;
+
+ String get xcrunPath => path.join('/usr', 'bin', 'xcrun');
+
+ String _getSimulatorPath() {
+ return path.join(homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', id);
+ }
+
+ String _getSimulatorAppHomeDirectory(ApplicationPackage app) {
+ String simulatorPath = _getSimulatorPath();
+ if (simulatorPath == null)
+ return null;
+ return path.join(simulatorPath, 'data');
+ }
+
+ @override
+ bool installApp(ApplicationPackage app) {
+ if (!isConnected())
+ return false;
+
+ try {
+ SimControl.install(id, app.localPath);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ @override
+ bool isConnected() {
+ if (!Platform.isMacOS)
+ return false;
+ return SimControl.getConnectedDevices().any((SimDevice device) => device.udid == id);
+ }
+
+ @override
+ bool isSupported() {
+ if (!Platform.isMacOS) {
+ _supportMessage = "Not supported on a non Mac host";
+ return false;
+ }
+
+ // Step 1: Check if the device is part of a blacklisted category.
+ // We do not support WatchOS or tvOS devices.
+
+ RegExp blacklist = new RegExp(r'Apple (TV|Watch)', caseSensitive: false);
+
+ if (blacklist.hasMatch(name)) {
+ _supportMessage = "Flutter does not support either the Apple TV or Watch. Choose an iPhone 5s or above.";
+ return false;
+ }
+
+ // Step 2: Check if the device must be rejected because of its version.
+ // There is an artitifical check on older simulators where arm64
+ // targetted applications cannot be run (even though the
+ // Flutter runner on the simulator is completely different).
+
+ RegExp versionExp = new RegExp(r'iPhone ([0-9])+');
+ Match match = versionExp.firstMatch(name);
+
+ if (match == null) {
+ // Not an iPhone. All available non-iPhone simulators are compatible.
+ return true;
+ }
+
+ if (int.parse(match.group(1)) > 5) {
+ // iPhones 6 and above are always fine.
+ return true;
+ }
+
+ // The 's' subtype of 5 is compatible.
+ if (name.contains('iPhone 5s')) {
+ return true;
+ }
+
+ _supportMessage = "The simulator version is too old. Choose an iPhone 5s or above.";
+ return false;
+ }
+
+ String _supportMessage;
+
+ @override
+ String supportMessage() {
+ if (isSupported()) {
+ return "Supported";
+ }
+
+ return _supportMessage != null ? _supportMessage : "Unknown";
+ }
+
+ @override
+ bool isAppInstalled(ApplicationPackage app) {
+ try {
+ String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
+ return FileSystemEntity.isDirectorySync(simulatorHomeDirectory);
+ } catch (e) {
+ return false;
+ }
+ }
+
+ @override
+ Future<bool> startApp(
+ ApplicationPackage app,
+ Toolchain toolchain, {
+ String mainPath,
+ String route,
+ bool checked: true,
+ bool clearLogs: false,
+ bool startPaused: false,
+ int debugPort: observatoryDefaultPort,
+ Map<String, dynamic> platformArgs
+ }) async {
+ // TODO(chinmaygarde): Use mainPath, route.
+ printTrace('Building ${app.name} for $id.');
+
+ if (clearLogs)
+ this.clearLogs();
+
+ // Step 1: Build the Xcode project.
+ bool buildResult = await buildIOSXcodeProject(app, buildForDevice: false);
+ if (!buildResult) {
+ printError('Could not build the application for the simulator.');
+ return false;
+ }
+
+ // Step 2: Assert that the Xcode project was successfully built.
+ Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphonesimulator', 'Runner.app'));
+ bool bundleExists = await bundle.exists();
+ if (!bundleExists) {
+ printError('Could not find the built application bundle at ${bundle.path}.');
+ return false;
+ }
+
+ // Step 3: Install the updated bundle to the simulator.
+ SimControl.install(id, path.absolute(bundle.path));
+
+ // Step 4: Prepare launch arguments.
+ List<String> args = <String>[];
+
+ if (checked)
+ args.add("--enable-checked-mode");
+
+ if (startPaused)
+ args.add("--start-paused");
+
+ if (debugPort != observatoryDefaultPort)
+ args.add("--observatory-port=$debugPort");
+
+ // Step 5: Launch the updated application in the simulator.
+ try {
+ SimControl.launch(id, app.id, args);
+ } catch (error) {
+ printError('$error');
+ return false;
+ }
+
+ printTrace('Successfully started ${app.name} on $id.');
+
+ return true;
+ }
+
+ @override
+ Future<bool> stopApp(ApplicationPackage app) async {
+ // Currently we don't have a way to stop an app running on iOS.
+ return false;
+ }
+
+ Future<bool> pushFile(
+ ApplicationPackage app, String localFile, String targetFile) async {
+ if (Platform.isMacOS) {
+ String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
+ runCheckedSync(<String>['cp', localFile, path.join(simulatorHomeDirectory, targetFile)]);
+ return true;
+ }
+ return false;
+ }
+
+ String get logFilePath {
+ return path.join(homeDirectory, 'Library', 'Logs', 'CoreSimulator', id, 'system.log');
+ }
+
+ @override
+ TargetPlatform get platform => TargetPlatform.iOSSimulator;
+
+ DeviceLogReader createLogReader() => new _IOSSimulatorLogReader(this);
+
+ void clearLogs() {
+ File logFile = new File(logFilePath);
+ if (logFile.existsSync()) {
+ RandomAccessFile randomFile = logFile.openSync(mode: FileMode.WRITE);
+ randomFile.truncateSync(0);
+ randomFile.closeSync();
+ }
+ }
+
+ void ensureLogsExists() {
+ File logFile = new File(logFilePath);
+ if (!logFile.existsSync())
+ logFile.writeAsBytesSync(<int>[]);
+ }
+}
+
+class _IOSSimulatorLogReader extends DeviceLogReader {
+ _IOSSimulatorLogReader(this.device);
+
+ final IOSSimulator device;
+
+ bool _lastWasFiltered = false;
+
+ String get name => device.name;
+
+ Future<int> logs({ bool clear: false }) async {
+ if (!device.isConnected())
+ return 2;
+
+ if (clear)
+ device.clearLogs();
+
+ device.ensureLogsExists();
+
+ // Match the log prefix (in order to shorten it):
+ // 'Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: ...'
+ RegExp mapRegex = new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$');
+ // Jan 31 19:23:28 --- last message repeated 1 time ---
+ RegExp lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ --- (.*) ---$');
+
+ // This filter matches many Flutter lines in the log:
+ // new RegExp(r'(FlutterRunner|flutter.runner.Runner|$id)'), but it misses
+ // a fair number, including ones that would be useful in diagnosing crashes.
+ // For now, we're not filtering the log file (but do clear it with each run).
+
+ Future<int> result = runCommandAndStreamOutput(
+ <String>['tail', '-n', '+0', '-F', device.logFilePath],
+ prefix: '[$name] ',
+ mapFunction: (String string) {
+ Match match = mapRegex.matchAsPrefix(string);
+ if (match != null) {
+ _lastWasFiltered = true;
+
+ // Filter out some messages that clearly aren't related to Flutter.
+ if (string.contains(': could not find icon for representation -> com.apple.'))
+ return null;
+ String category = match.group(1);
+ String content = match.group(2);
+ if (category == 'Game Center' || category == 'itunesstored' || category == 'nanoregistrylaunchd' ||
+ category == 'mstreamd' || category == 'syncdefaultsd' || category == 'companionappd' ||
+ category == 'searchd')
+ return null;
+
+ _lastWasFiltered = false;
+
+ if (category == 'Runner')
+ return content;
+ return '$category: $content';
+ }
+ match = lastMessageRegex.matchAsPrefix(string);
+ if (match != null && !_lastWasFiltered)
+ return '(${match.group(1)})';
+ return string;
+ }
+ );
+
+ // Track system.log crashes.
+ // ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
+ runCommandAndStreamOutput(
+ <String>['tail', '-F', '/private/var/log/system.log'],
+ prefix: '[$name] ',
+ filter: new RegExp(r' FlutterRunner\[\d+\] '),
+ mapFunction: (String string) {
+ Match match = mapRegex.matchAsPrefix(string);
+ return match == null ? string : '${match.group(1)}: ${match.group(2)}';
+ }
+ );
+
+ return await result;
+ }
+
+ int get hashCode => device.logFilePath.hashCode;
+
+ bool operator ==(dynamic other) {
+ if (identical(this, other))
+ return true;
+ if (other is! _IOSSimulatorLogReader)
+ return false;
+ return other.device.logFilePath == device.logFilePath;
+ }
+}
diff --git a/packages/flutter_tools/test/android_device_test.dart b/packages/flutter_tools/test/android_device_test.dart
index d71878d..2ad4701 100644
--- a/packages/flutter_tools/test/android_device_test.dart
+++ b/packages/flutter_tools/test/android_device_test.dart
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'package:flutter_tools/src/android/device_android.dart';
+import 'package:flutter_tools/src/android/android_device.dart';
import 'package:test/test.dart';
import 'src/context.dart';
diff --git a/packages/flutter_tools/test/base_utils_test.dart b/packages/flutter_tools/test/base_utils_test.dart
new file mode 100644
index 0000000..053fdd4
--- /dev/null
+++ b/packages/flutter_tools/test/base_utils_test.dart
@@ -0,0 +1,37 @@
+// Copyright 2016 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 'package:flutter_tools/src/base/utils.dart';
+import 'package:test/test.dart';
+
+main() => defineTests();
+
+defineTests() {
+ group('ItemListNotifier', () {
+ test('sends notifications', () async {
+ ItemListNotifier<String> list = new ItemListNotifier<String>();
+ expect(list.items, isEmpty);
+
+ Future<List<String>> addedStreamItems = list.onAdded.toList();
+ Future<List<String>> removedStreamItems = list.onRemoved.toList();
+
+ list.updateWithNewList(<String>['aaa']);
+ list.updateWithNewList(<String>['aaa', 'bbb']);
+ list.updateWithNewList(<String>['bbb']);
+ list.dispose();
+
+ List<String> addedItems = await addedStreamItems;
+ List<String> removedItems = await removedStreamItems;
+
+ expect(addedItems.length, 2);
+ expect(addedItems.first, 'aaa');
+ expect(addedItems[1], 'bbb');
+
+ expect(removedItems.length, 1);
+ expect(removedItems.first, 'aaa');
+ });
+ });
+}
diff --git a/packages/flutter_tools/test/daemon_test.dart b/packages/flutter_tools/test/daemon_test.dart
index 70035a9..29ff62f 100644
--- a/packages/flutter_tools/test/daemon_test.dart
+++ b/packages/flutter_tools/test/daemon_test.dart
@@ -3,11 +3,14 @@
// found in the LICENSE file.
import 'dart:async';
+import 'dart:io';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/commands/daemon.dart';
+import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/globals.dart';
+import 'package:flutter_tools/src/ios/mac.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
@@ -31,6 +34,9 @@
appContext = new AppContext();
notifyingLogger = new NotifyingLogger();
appContext[Logger] = notifyingLogger;
+ appContext[Doctor] = new Doctor();
+ if (Platform.isMacOS)
+ appContext[XCode] = new XCode();
});
tearDown(() {
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index a5f3ee3..8936cf9 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -2,11 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'package:flutter_tools/src/android/device_android.dart';
+import 'package:flutter_tools/src/android/android_device.dart';
import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/build_configuration.dart';
import 'package:flutter_tools/src/device.dart';
-import 'package:flutter_tools/src/ios/device_ios.dart';
+import 'package:flutter_tools/src/ios/devices.dart';
+import 'package:flutter_tools/src/ios/simulators.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/toolchain.dart';
import 'package:mockito/mockito.dart';