android_device.dart 20.4 KB
Newer Older
1 2 3 4 5
// 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';
6
import 'dart:convert';
7 8 9 10
import 'dart:io';

import 'package:path/path.dart' as path;

11
import '../android/android_sdk.dart';
12
import '../application_package.dart';
13 14
import '../base/common.dart';
import '../base/os.dart';
15 16 17 18
import '../base/process.dart';
import '../build_configuration.dart';
import '../device.dart';
import '../flx.dart' as flx;
19
import '../globals.dart';
20
import '../service_protocol.dart';
21
import '../toolchain.dart';
22
import 'adb.dart';
23 24
import 'android.dart';

25 26
const String _defaultAdbPath = 'adb';

27 28 29 30 31 32
// Path where the FLX bundle will be copied on the device.
const String _deviceBundlePath = '/data/local/tmp/dev.flx';

// Path where the snapshot will be copied on the device.
const String _deviceSnapshotPath = '/data/local/tmp/dev_snapshot.bin';

33 34
class AndroidDevices extends PollingDeviceDiscovery {
  AndroidDevices() : super('AndroidDevices');
35

36
  @override
37
  bool get supportsPlatform => true;
38 39

  @override
40
  List<Device> pollingGetDevices() => getAdbDevices();
41 42
}

43
class AndroidDevice extends Device {
44 45 46 47
  AndroidDevice(
    String id, {
    this.productID,
    this.modelID,
48 49
    this.deviceCodeName
  }) : super(id);
50

51 52 53 54
  final String productID;
  final String modelID;
  final String deviceCodeName;

55 56
  bool _isLocalEmulator;

57
  @override
58 59
  bool get isLocalEmulator {
    if (_isLocalEmulator == null) {
60
      // http://developer.android.com/ndk/guides/abis.html (x86, armeabi-v7a, ...)
61
      String value = runCheckedSync(adbCommandForDevice(['shell', 'getprop', 'ro.product.cpu.abi']));
62
      _isLocalEmulator = value.startsWith('x86');
63 64 65 66
    }

    return _isLocalEmulator;
  }
67

68
  _AdbLogReader _logReader;
69
  _AndroidDevicePortForwarder _portForwarder;
70

71
  List<String> adbCommandForDevice(List<String> args) {
72
    return <String>[androidSdk.adbPath, '-s', id]..addAll(args);
73 74 75 76
  }

  bool _isValidAdbVersion(String adbVersion) {
    // Sample output: 'Android Debug Bridge version 1.0.31'
77
    Match versionFields = new RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
    if (versionFields != null) {
      int majorVersion = int.parse(versionFields[1]);
      int minorVersion = int.parse(versionFields[2]);
      int patchVersion = int.parse(versionFields[3]);
      if (majorVersion > 1) {
        return true;
      }
      if (majorVersion == 1 && minorVersion > 0) {
        return true;
      }
      if (majorVersion == 1 && minorVersion == 0 && patchVersion >= 32) {
        return true;
      }
      return false;
    }
93
    printError(
94 95 96 97
        'Unrecognized adb version string $adbVersion. Skipping version check.');
    return true;
  }

98 99 100 101
  bool _checkForSupportedAdbVersion() {
    if (androidSdk == null)
      return false;

102
    try {
103 104
      String adbVersion = runCheckedSync(<String>[androidSdk.adbPath, 'version']);
      if (_isValidAdbVersion(adbVersion))
105
        return true;
106 107 108
      printError('The ADB at "${androidSdk.adbPath}" is too old; please install version 1.0.32 or later.');
    } catch (error, trace) {
      printError('Error running ADB: $error', trace);
109
    }
110

111 112 113 114 115 116 117 118 119
    return false;
  }

  bool _checkForSupportedAndroidVersion() {
    try {
      // If the server is automatically restarted, then we get irrelevant
      // output lines like this, which we want to ignore:
      //   adb server is out of date.  killing..
      //   * daemon started successfully *
120
      runCheckedSync(<String>[androidSdk.adbPath, 'start-server']);
121 122

      // Sample output: '22'
123 124 125
      String sdkVersion = runCheckedSync(
        adbCommandForDevice(<String>['shell', 'getprop', 'ro.build.version.sdk'])
      ).trimRight();
126

127
      int sdkVersionParsed = int.parse(sdkVersion, onError: (String source) => null);
128
      if (sdkVersionParsed == null) {
129
        printError('Unexpected response from getprop: "$sdkVersion"');
130 131
        return false;
      }
132

133
      if (sdkVersionParsed < minApiLevel) {
134
        printError(
135 136 137 138
          'The Android version ($sdkVersion) on the target device is too old. Please '
          'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.');
        return false;
      }
139

140 141
      return true;
    } catch (e) {
142
      printError('Unexpected failure from adb: $e');
143
      return false;
144 145 146 147 148 149 150 151
    }
  }

  String _getDeviceSha1Path(ApplicationPackage app) {
    return '/data/local/tmp/sky.${app.id}.sha1';
  }

  String _getDeviceApkSha1(ApplicationPackage app) {
152
    return runCheckedSync(adbCommandForDevice(<String>['shell', 'cat', _getDeviceSha1Path(app)]));
153 154 155
  }

  String _getSourceSha1(ApplicationPackage app) {
Devon Carew's avatar
Devon Carew committed
156 157
    File shaFile = new File('${app.localPath}.sha1');
    return shaFile.existsSync() ? shaFile.readAsStringSync() : '';
158 159
  }

160
  @override
161 162 163 164
  String get name => modelID;

  @override
  bool isAppInstalled(ApplicationPackage app) {
165 166 167 168 169
    // This call takes 400ms - 600ms.
    if (runCheckedSync(adbCommandForDevice(['shell', 'pm', 'path', app.id])).isEmpty)
      return false;

    // Check the application SHA.
Devon Carew's avatar
Devon Carew committed
170
    return _getDeviceApkSha1(app) == _getSourceSha1(app);
171 172 173 174 175
  }

  @override
  bool installApp(ApplicationPackage app) {
    if (!FileSystemEntity.isFileSync(app.localPath)) {
176
      printError('"${app.localPath}" does not exist.');
177 178 179
      return false;
    }

180 181 182
    if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion())
      return false;

183 184 185
    printStatus('Installing ${app.name} on device.');
    runCheckedSync(adbCommandForDevice(<String>['install', '-r', app.localPath]));
    runCheckedSync(adbCommandForDevice(<String>['shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app)]));
186 187 188
    return true;
  }

189 190
  Future<Null> _forwardObservatoryPort(int devicePort, int port) async {
    bool portWasZero = (port == null) || (port == 0);
191

192
    try {
193
      // Set up port forwarding for observatory.
194
      port = await portForwarder.forward(devicePort,
195
                                         hostPort: port);
196 197
      if (portWasZero)
        printStatus('Observatory listening on http://127.0.0.1:$port');
198
    } catch (e) {
199
      printError('Unable to forward Observatory port $port: $e');
200 201 202
    }
  }

203
  Future<bool> startBundle(AndroidApk apk, String bundlePath, {
204 205 206
    bool checked: true,
    bool traceStartup: false,
    String route,
207 208 209 210
    bool clearLogs: false,
    bool startPaused: false,
    int debugPort: observatoryDefaultPort
  }) async {
211
    printTrace('$this startBundle');
212 213

    if (!FileSystemEntity.isFileSync(bundlePath)) {
214
      printError('Cannot find $bundlePath');
215 216 217 218 219 220
      return false;
    }

    if (clearLogs)
      this.clearLogs();

221
    runCheckedSync(adbCommandForDevice(<String>['push', bundlePath, _deviceBundlePath]));
222

223 224 225
    ServiceProtocolDiscovery serviceProtocolDiscovery =
        new ServiceProtocolDiscovery(logReader);

Devon Carew's avatar
Devon Carew committed
226 227 228
    // We take this future here but do not wait for completion until *after* we
    // start the bundle.
    Future<int> scrapeServicePort = serviceProtocolDiscovery.nextPort();
229

230
    List<String> cmd = adbCommandForDevice(<String>[
231 232
      'shell', 'am', 'start',
      '-a', 'android.intent.action.RUN',
233
      '-d', _deviceBundlePath,
234
      '-f', '0x20000000',  // FLAG_ACTIVITY_SINGLE_TOP
235
      '--ez', 'enable-background-compilation', 'true',
236 237
    ]);
    if (checked)
238
      cmd.addAll(<String>['--ez', 'enable-checked-mode', 'true']);
239
    if (traceStartup)
240
      cmd.addAll(<String>['--ez', 'trace-startup', 'true']);
241
    if (startPaused)
242
      cmd.addAll(<String>['--ez', 'start-paused', 'true']);
243
    if (route != null)
244
      cmd.addAll(<String>['--es', 'route', route]);
245
    cmd.add(apk.launchActivity);
246 247 248 249 250 251
    String result = runCheckedSync(cmd);
    // This invocation returns 0 even when it fails.
    if (result.contains('Error: ')) {
      printError(result.trim());
      return false;
    }
252

Devon Carew's avatar
Devon Carew committed
253 254 255
    // Wait for the service protocol port here. This will complete once the
    // device has printed "Observatory is listening on...".
    printTrace('Waiting for observatory port to be available...');
256

Devon Carew's avatar
Devon Carew committed
257 258 259 260 261 262 263 264 265 266 267 268 269
    try {
      int devicePort = await scrapeServicePort.timeout(new Duration(seconds: 12));
      printTrace('service protocol port = $devicePort');
      await _forwardObservatoryPort(devicePort, debugPort);
      return true;
    } catch (error) {
      if (error is TimeoutException)
        printError('Timed out while waiting for a debug connection.');
      else
        printError('Error waiting for a debug connection: $error');

      return false;
    }
270 271 272 273 274 275 276 277 278
  }

  @override
  Future<bool> startApp(
    ApplicationPackage package,
    Toolchain toolchain, {
    String mainPath,
    String route,
    bool checked: true,
Devon Carew's avatar
Devon Carew committed
279
    bool clearLogs: false,
280 281
    bool startPaused: false,
    int debugPort: observatoryDefaultPort,
282
    Map<String, dynamic> platformArgs
283
  }) async {
284 285 286
    if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion())
      return false;

Devon Carew's avatar
Devon Carew committed
287
    String localBundlePath = await flx.buildFlx(
288
      toolchain,
289 290
      mainPath: mainPath,
      includeRobotoFonts: false
291 292 293 294
    );

    printTrace('Starting bundle for $this.');

Devon Carew's avatar
Devon Carew committed
295 296 297 298 299 300 301 302 303 304 305 306 307
    if (await startBundle(
      package,
      localBundlePath,
      checked: checked,
      traceStartup: platformArgs['trace-startup'],
      route: route,
      clearLogs: clearLogs,
      startPaused: startPaused,
      debugPort: debugPort
    )) {
      return true;
    } else {
      return false;
308
    }
309 310
  }

311
  @override
312 313 314
  Future<bool> stopApp(ApplicationPackage app) {
    List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]);
    return runCommandAndStreamOutput(command).then((int exitCode) => exitCode == 0);
315 316
  }

Devon Carew's avatar
Devon Carew committed
317
  // TODO(devoncarew): Use isLocalEmulator to return android_arm or android_x64.
318
  @override
319
  TargetPlatform get platform => TargetPlatform.android_arm;
320

321
  @override
322
  void clearLogs() {
323
    runSync(adbCommandForDevice(<String>['logcat', '-c']));
324 325
  }

326
  @override
327 328 329 330 331
  DeviceLogReader get logReader {
    if (_logReader == null)
      _logReader = new _AdbLogReader(this);
    return _logReader;
  }
332

333
  @override
334 335 336 337 338 339 340
  DevicePortForwarder get portForwarder {
    if (_portForwarder == null)
      _portForwarder = new _AndroidDevicePortForwarder(this);

    return _portForwarder;
  }

341
  void startTracing(AndroidApk apk) {
342
    runCheckedSync(adbCommandForDevice(<String>[
343 344 345 346 347 348 349 350
      'shell',
      'am',
      'broadcast',
      '-a',
      '${apk.id}.TRACING_START'
    ]));
  }

351 352
  /// Return the most recent timestamp in the Android log or `null` if there is
  /// no available timestamp. The format can be passed to logcat's -T option.
353
  String get lastLogcatTimestamp {
354
    String output = runCheckedSync(adbCommandForDevice(<String>[
355
      'logcat', '-v', 'time', '-t', '1'
356
    ]));
357 358 359

    RegExp timeRegExp = new RegExp(r'^\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}', multiLine: true);
    Match timeMatch = timeRegExp.firstMatch(output);
360
    return timeMatch?.group(0);
361 362
  }

363
  Future<String> stopTracing(AndroidApk apk, { String outPath }) async {
364 365
    // Workaround for logcat -c not always working:
    // http://stackoverflow.com/questions/25645012/logcat-on-android-l-not-clearing-after-unplugging-and-reconnecting
366
    String beforeStop = lastLogcatTimestamp;
367
    runCheckedSync(adbCommandForDevice(<String>[
368 369 370 371 372 373 374 375 376 377
      'shell',
      'am',
      'broadcast',
      '-a',
      '${apk.id}.TRACING_STOP'
    ]));

    RegExp traceRegExp = new RegExp(r'Saving trace to (\S+)', multiLine: true);
    RegExp completeRegExp = new RegExp(r'Trace complete', multiLine: true);

Ian Hickson's avatar
Ian Hickson committed
378
    String tracePath;
379 380
    bool isComplete = false;
    while (!isComplete) {
381
      List<String> args = <String>['logcat', '-d'];
382 383 384
      if (beforeStop != null)
        args.addAll(<String>['-T', beforeStop]);
      String logs = runCheckedSync(adbCommandForDevice(args));
385 386 387 388 389 390 391 392 393
      Match fileMatch = traceRegExp.firstMatch(logs);
      if (fileMatch != null && fileMatch[1] != null) {
        tracePath = fileMatch[1];
      }
      isComplete = completeRegExp.hasMatch(logs);
    }

    if (tracePath != null) {
      String localPath = (outPath != null) ? outPath : path.basename(tracePath);
394

395
      // Run cat via ADB to print the captured trace file. (adb pull will be unable
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
      // to access the file if it does not have root permissions)
      IOSink catOutput = new File(localPath).openWrite();
      List<String> catCommand = adbCommandForDevice(
          <String>['shell', 'run-as', apk.id, 'cat', tracePath]
      );
      Process catProcess = await Process.start(catCommand[0],
          catCommand.getRange(1, catCommand.length).toList());
      catProcess.stdout.pipe(catOutput);
      int exitCode = await catProcess.exitCode;
      if (exitCode != 0)
        throw 'Error code $exitCode returned when running ${catCommand.join(" ")}';

      runSync(adbCommandForDevice(
          <String>['shell', 'run-as', apk.id, 'rm', tracePath]
      ));
411 412
      return localPath;
    }
413
    printError('No trace file detected. '
414 415 416 417
        'Did you remember to start the trace before stopping it?');
    return null;
  }

418
  @override
419 420
  bool isSupported() => true;

421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
  Future<bool> refreshSnapshot(AndroidApk apk, String snapshotPath) async {
    if (!FileSystemEntity.isFileSync(snapshotPath)) {
      printError('Cannot find $snapshotPath');
      return false;
    }

    runCheckedSync(adbCommandForDevice(<String>['push', snapshotPath, _deviceSnapshotPath]));

    List<String> cmd = adbCommandForDevice(<String>[
      'shell', 'am', 'start',
      '-a', 'android.intent.action.RUN',
      '-d', _deviceBundlePath,
      '-f', '0x20000000',  // FLAG_ACTIVITY_SINGLE_TOP
      '--es', 'snapshot', _deviceSnapshotPath,
      apk.launchActivity,
    ]);
    runCheckedSync(cmd);
    return true;
  }
Devon Carew's avatar
Devon Carew committed
440 441 442 443 444 445 446 447 448 449 450 451 452 453

  @override
  bool get supportsScreenshot => true;

  @override
  Future<bool> takeScreenshot(File outputFile) {
    const String remotePath = '/data/local/tmp/flutter_screenshot.png';

    runCheckedSync(adbCommandForDevice(<String>['shell', 'screencap', '-p', remotePath]));
    runCheckedSync(adbCommandForDevice(<String>['pull', remotePath, outputFile.path]));
    runCheckedSync(adbCommandForDevice(<String>['shell', 'rm', remotePath]));

    return new Future<bool>.value(true);
  }
454
}
455

456
List<AndroidDevice> getAdbDevices() {
457 458
  String adbPath = getAdbPath(androidSdk);
  if (adbPath == null)
459
    return <AndroidDevice>[];
460

461
  List<AndroidDevice> devices = [];
462

463
  List<String> output = runSync(<String>[adbPath, 'devices', '-l']).trim().split('\n');
464 465 466 467 468 469 470 471 472 473

  // 015d172c98400a03       device usb:340787200X product:nakasi model:Nexus_7 device:grouper
  RegExp deviceRegex1 = new RegExp(
      r'^(\S+)\s+device\s+.*product:(\S+)\s+model:(\S+)\s+device:(\S+)$');

  // 0149947A0D01500C       device usb:340787200X
  RegExp deviceRegex2 = new RegExp(r'^(\S+)\s+device\s+\S+$');
  RegExp unauthorizedRegex = new RegExp(r'^(\S+)\s+unauthorized\s+\S+$');
  RegExp offlineRegex = new RegExp(r'^(\S+)\s+offline\s+\S+$');

474
  // Skip the first line, which is always 'List of devices attached'.
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492
  for (String line in output.skip(1)) {
    // Skip lines like:
    // * daemon not running. starting it now on port 5037 *
    // * daemon started successfully *
    if (line.startsWith('* daemon '))
      continue;

    if (line.startsWith('List of devices'))
      continue;

    if (deviceRegex1.hasMatch(line)) {
      Match match = deviceRegex1.firstMatch(line);
      String deviceID = match[1];
      String productID = match[2];
      String modelID = match[3];
      String deviceCodeName = match[4];

      if (modelID != null)
493
        modelID = cleanAdbDeviceName(modelID);
494 495

      devices.add(new AndroidDevice(
496 497 498
        deviceID,
        productID: productID,
        modelID: modelID,
499
        deviceCodeName: deviceCodeName
500 501 502 503
      ));
    } else if (deviceRegex2.hasMatch(line)) {
      Match match = deviceRegex2.firstMatch(line);
      String deviceID = match[1];
504
      devices.add(new AndroidDevice(deviceID));
505 506 507
    } else if (unauthorizedRegex.hasMatch(line)) {
      Match match = unauthorizedRegex.firstMatch(line);
      String deviceID = match[1];
508
      printError(
509 510 511 512 513 514
        'Device $deviceID is not authorized.\n'
        'You might need to check your device for an authorization dialog.'
      );
    } else if (offlineRegex.hasMatch(line)) {
      Match match = offlineRegex.firstMatch(line);
      String deviceID = match[1];
515
      printError('Device $deviceID is offline.');
516
    } else {
517
      printError(
518 519 520 521 522 523 524 525
        'Unexpected failure parsing device information from adb output:\n'
        '$line\n'
        'Please report a bug at https://github.com/flutter/flutter/issues/new');
    }
  }
  return devices;
}

526
/// A log reader that logs from `adb logcat`.
Devon Carew's avatar
Devon Carew committed
527 528 529 530 531
class _AdbLogReader extends DeviceLogReader {
  _AdbLogReader(this.device);

  final AndroidDevice device;

532 533 534 535
  final StreamController<String> _linesStreamController =
      new StreamController<String>.broadcast();

  Process _process;
Ian Hickson's avatar
Ian Hickson committed
536 537
  StreamSubscription<String> _stdoutSubscription;
  StreamSubscription<String> _stderrSubscription;
538

539
  @override
540 541
  Stream<String> get lines => _linesStreamController.stream;

542
  @override
543
  String get name => device.name;
Devon Carew's avatar
Devon Carew committed
544

545
  @override
546 547
  bool get isReading => _process != null;

548
  @override
Ian Hickson's avatar
Ian Hickson committed
549
  Future<int> get finished => _process != null ? _process.exitCode : new Future<int>.value(0);
550

551
  @override
Ian Hickson's avatar
Ian Hickson committed
552
  Future<Null> start() async {
553 554
    if (_process != null)
      throw new StateError('_AdbLogReader must be stopped before it can be started.');
555 556

    // Start the adb logcat process.
557
    List<String> args = <String>['logcat', '-v', 'tag'];
558 559 560
    String lastTimestamp = device.lastLogcatTimestamp;
    if (lastTimestamp != null)
      args.addAll(<String>['-T', lastTimestamp]);
Devon Carew's avatar
Devon Carew committed
561 562 563
    args.addAll(<String>[
      '-s', 'flutter:V', 'SkyMain:V', 'AndroidRuntime:W', 'ActivityManager:W', 'System.err:W', '*:F'
    ]);
564
    _process = await runCommand(device.adbCommandForDevice(args));
565 566 567 568 569 570 571 572 573
    _stdoutSubscription =
        _process.stdout.transform(UTF8.decoder)
                       .transform(const LineSplitter()).listen(_onLine);
    _stderrSubscription =
        _process.stderr.transform(UTF8.decoder)
                       .transform(const LineSplitter()).listen(_onLine);
    _process.exitCode.then(_onExit);
  }

574
  @override
Ian Hickson's avatar
Ian Hickson committed
575
  Future<Null> stop() async {
576 577 578
    if (_process == null)
      throw new StateError('_AdbLogReader must be started before it can be stopped.');

579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595
    _stdoutSubscription?.cancel();
    _stdoutSubscription = null;
    _stderrSubscription?.cancel();
    _stderrSubscription = null;
    await _process.kill();
    _process = null;
  }

  void _onExit(int exitCode) {
    _stdoutSubscription?.cancel();
    _stdoutSubscription = null;
    _stderrSubscription?.cancel();
    _stderrSubscription = null;
    _process = null;
  }

  void _onLine(String line) {
596 597 598 599
    // Filter out some noisy ActivityManager notifications.
    if (line.startsWith('W/ActivityManager: getRunningAppProcesses'))
      return;

600
    _linesStreamController.add(line);
Devon Carew's avatar
Devon Carew committed
601 602
  }

603
  @override
Devon Carew's avatar
Devon Carew committed
604 605
  int get hashCode => name.hashCode;

606
  @override
Devon Carew's avatar
Devon Carew committed
607 608 609
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
610 611 612
    if (other is! _AdbLogReader)
      return false;
    return other.device.id == device.id;
Devon Carew's avatar
Devon Carew committed
613 614
  }
}
615 616 617 618 619 620 621 622 623 624

class _AndroidDevicePortForwarder extends DevicePortForwarder {
  _AndroidDevicePortForwarder(this.device);

  final AndroidDevice device;

  static int _extractPort(String portString) {
    return int.parse(portString.trim(), onError: (_) => null);
  }

625
  @override
626 627 628
  List<ForwardedPort> get forwardedPorts {
    final List<ForwardedPort> ports = <ForwardedPort>[];

629 630 631
    String stdout = runCheckedSync(device.adbCommandForDevice(
      <String>['forward', '--list']
    ));
632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656

    List<String> lines = LineSplitter.split(stdout).toList();
    for (String line in lines) {
      if (line.startsWith(device.id)) {
        List<String> splitLine = line.split("tcp:");

        // Sanity check splitLine.
        if (splitLine.length != 3)
          continue;

        // Attempt to extract ports.
        int hostPort = _extractPort(splitLine[1]);
        int devicePort = _extractPort(splitLine[2]);

        // Failed, skip.
        if ((hostPort == null) || (devicePort == null))
          continue;

        ports.add(new ForwardedPort(hostPort, devicePort));
      }
    }

    return ports;
  }

657
  @override
658
  Future<int> forward(int devicePort, { int hostPort }) async {
659 660 661 662 663
    if ((hostPort == null) || (hostPort == 0)) {
      // Auto select host port.
      hostPort = await findAvailablePort();
    }

664 665 666
    runCheckedSync(device.adbCommandForDevice(
      <String>['forward', 'tcp:$hostPort', 'tcp:$devicePort']
    ));
667 668 669 670

    return hostPort;
  }

671
  @override
Ian Hickson's avatar
Ian Hickson committed
672
  Future<Null> unforward(ForwardedPort forwardedPort) async {
673 674 675
    runCheckedSync(device.adbCommandForDevice(
      <String>['forward', '--remove', 'tcp:${forwardedPort.hostPort}']
    ));
676 677
  }
}