android_device.dart 18.8 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
import 'dart:io';

9
import '../android/android_sdk.dart';
10
import '../application_package.dart';
11
import '../base/os.dart';
12
import '../base/process.dart';
13
import '../build_info.dart';
14 15
import '../device.dart';
import '../flx.dart' as flx;
16
import '../globals.dart';
17
import '../service_protocol.dart';
18
import 'adb.dart';
19 20
import 'android.dart';

21 22
const String _defaultAdbPath = 'adb';

23 24 25 26 27 28
// 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';

29 30
class AndroidDevices extends PollingDeviceDiscovery {
  AndroidDevices() : super('AndroidDevices');
31

32
  @override
33
  bool get supportsPlatform => true;
34 35

  @override
36
  List<Device> pollingGetDevices() => getAdbDevices();
37 38
}

39
class AndroidDevice extends Device {
40 41 42 43
  AndroidDevice(
    String id, {
    this.productID,
    this.modelID,
44 45
    this.deviceCodeName
  }) : super(id);
46

47 48 49 50
  final String productID;
  final String modelID;
  final String deviceCodeName;

51
  Map<String, String> _properties;
52
  bool _isLocalEmulator;
53 54 55 56 57 58 59 60 61 62 63 64 65
  TargetPlatform _platform;

  String _getProperty(String name) {
    if (_properties == null) {
      String getpropOutput = runCheckedSync(adbCommandForDevice(<String>['shell', 'getprop']));
      RegExp propertyExp = new RegExp(r'\[(.*?)\]: \[(.*?)\]');
      _properties = <String, String>{};
      for (Match m in propertyExp.allMatches(getpropOutput)) {
        _properties[m.group(1)] = m.group(2);
      }
    }
    return _properties[name];
  }
66

67
  @override
68 69
  bool get isLocalEmulator {
    if (_isLocalEmulator == null) {
70 71 72 73 74 75 76 77 78
      String characteristics = _getProperty('ro.build.characteristics');
      _isLocalEmulator = characteristics != null && characteristics.contains('emulator');
    }
    return _isLocalEmulator;
  }

  @override
  TargetPlatform get platform {
    if (_platform == null) {
79
      // http://developer.android.com/ndk/guides/abis.html (x86, armeabi-v7a, ...)
80 81 82 83 84 85 86 87 88 89
      switch (_getProperty('ro.product.cpu.abi')) {
        case 'x86_64':
          _platform = TargetPlatform.android_x64;
          break;
        case 'x86':
          _platform = TargetPlatform.android_x86;
          break;
        default:
          _platform = TargetPlatform.android_arm;
          break;
90
      }
91 92
    }

93
    return _platform;
94
  }
95

96
  _AdbLogReader _logReader;
97
  _AndroidDevicePortForwarder _portForwarder;
98

99
  List<String> adbCommandForDevice(List<String> args) {
100
    return <String>[androidSdk.adbPath, '-s', id]..addAll(args);
101 102 103 104
  }

  bool _isValidAdbVersion(String adbVersion) {
    // Sample output: 'Android Debug Bridge version 1.0.31'
105
    Match versionFields = new RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
    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;
    }
121
    printError(
122 123 124 125
        'Unrecognized adb version string $adbVersion. Skipping version check.');
    return true;
  }

126 127 128 129
  bool _checkForSupportedAdbVersion() {
    if (androidSdk == null)
      return false;

130
    try {
131 132
      String adbVersion = runCheckedSync(<String>[androidSdk.adbPath, 'version']);
      if (_isValidAdbVersion(adbVersion))
133
        return true;
134 135 136
      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);
137
    }
138

139 140 141 142 143 144 145 146 147
    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 *
148
      runCheckedSync(<String>[androidSdk.adbPath, 'start-server']);
149 150

      // Sample output: '22'
151
      String sdkVersion = _getProperty('ro.build.version.sdk');
152

153
      int sdkVersionParsed = int.parse(sdkVersion, onError: (String source) => null);
154
      if (sdkVersionParsed == null) {
155
        printError('Unexpected response from getprop: "$sdkVersion"');
156 157
        return false;
      }
158

159
      if (sdkVersionParsed < minApiLevel) {
160
        printError(
161 162 163 164
          'The Android version ($sdkVersion) on the target device is too old. Please '
          'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.');
        return false;
      }
165

166 167
      return true;
    } catch (e) {
168
      printError('Unexpected failure from adb: $e');
169
      return false;
170 171 172 173 174 175 176 177
    }
  }

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

  String _getDeviceApkSha1(ApplicationPackage app) {
178
    return runCheckedSync(adbCommandForDevice(<String>['shell', 'cat', _getDeviceSha1Path(app)]));
179 180 181
  }

  String _getSourceSha1(ApplicationPackage app) {
Devon Carew's avatar
Devon Carew committed
182 183
    File shaFile = new File('${app.localPath}.sha1');
    return shaFile.existsSync() ? shaFile.readAsStringSync() : '';
184 185
  }

186
  @override
187 188 189 190
  String get name => modelID;

  @override
  bool isAppInstalled(ApplicationPackage app) {
191
    // This call takes 400ms - 600ms.
192 193
    String listOut = runCheckedSync(adbCommandForDevice(<String>['shell', 'pm', 'list', 'packages', app.id]));
    if (!LineSplitter.split(listOut).contains("package:${app.id}"))
194 195 196
      return false;

    // Check the application SHA.
Devon Carew's avatar
Devon Carew committed
197
    return _getDeviceApkSha1(app) == _getSourceSha1(app);
198 199 200 201 202
  }

  @override
  bool installApp(ApplicationPackage app) {
    if (!FileSystemEntity.isFileSync(app.localPath)) {
203
      printError('"${app.localPath}" does not exist.');
204 205 206
      return false;
    }

207 208 209
    if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion())
      return false;

210 211 212 213 214 215 216 217
    String installOut = runCheckedSync(adbCommandForDevice(<String>['install', '-r', app.localPath]));
    RegExp failureExp = new RegExp(r'^Failure.*$', multiLine: true);
    String failure = failureExp.stringMatch(installOut);
    if (failure != null) {
      printError('Package install error: $failure');
      return false;
    }

218
    runCheckedSync(adbCommandForDevice(<String>['shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app)]));
219 220 221
    return true;
  }

222
  Future<Null> _forwardPort(String service, int devicePort, int port) async {
223
    try {
224
      // Set up port forwarding for observatory.
Devon Carew's avatar
Devon Carew committed
225 226
      port = await portForwarder.forward(devicePort, hostPort: port);
      printStatus('$service listening on http://127.0.0.1:$port');
227
    } catch (e) {
228
      printError('Unable to forward port $port: $e');
229 230 231
    }
  }

Devon Carew's avatar
Devon Carew committed
232
  Future<LaunchResult> startBundle(AndroidApk apk, String bundlePath, {
233 234
    bool traceStartup: false,
    String route,
Devon Carew's avatar
Devon Carew committed
235
    DebuggingOptions options
236
  }) async {
237
    printTrace('$this startBundle');
238 239

    if (!FileSystemEntity.isFileSync(bundlePath)) {
240
      printError('Cannot find $bundlePath');
Devon Carew's avatar
Devon Carew committed
241
      return new LaunchResult.failed();
242 243
    }

244
    runCheckedSync(adbCommandForDevice(<String>['push', bundlePath, _deviceBundlePath]));
245

Devon Carew's avatar
Devon Carew committed
246 247
    ServiceProtocolDiscovery observatoryDiscovery;
    ServiceProtocolDiscovery diagnosticDiscovery;
248

Devon Carew's avatar
Devon Carew committed
249 250 251 252 253 254
    if (options.debuggingEnabled) {
      observatoryDiscovery = new ServiceProtocolDiscovery(
        logReader, ServiceProtocolDiscovery.kObservatoryService);
      diagnosticDiscovery = new ServiceProtocolDiscovery(
        logReader, ServiceProtocolDiscovery.kDiagnosticService);
    }
255

256
    List<String> cmd = adbCommandForDevice(<String>[
257 258
      'shell', 'am', 'start',
      '-a', 'android.intent.action.RUN',
259
      '-d', _deviceBundlePath,
260
      '-f', '0x20000000',  // FLAG_ACTIVITY_SINGLE_TOP
261
      '--ez', 'enable-background-compilation', 'true',
262 263
    ]);
    if (traceStartup)
264
      cmd.addAll(<String>['--ez', 'trace-startup', 'true']);
265
    if (route != null)
266
      cmd.addAll(<String>['--es', 'route', route]);
Devon Carew's avatar
Devon Carew committed
267 268 269 270 271 272
    if (options.debuggingEnabled) {
      if (options.checked)
        cmd.addAll(<String>['--ez', 'enable-checked-mode', 'true']);
      if (options.startPaused)
        cmd.addAll(<String>['--ez', 'start-paused', 'true']);
    }
273
    cmd.add(apk.launchActivity);
274 275 276 277
    String result = runCheckedSync(cmd);
    // This invocation returns 0 even when it fails.
    if (result.contains('Error: ')) {
      printError(result.trim());
Devon Carew's avatar
Devon Carew committed
278
      return new LaunchResult.failed();
279
    }
280

Devon Carew's avatar
Devon Carew committed
281 282 283 284 285 286
    if (!options.debuggingEnabled) {
      return new LaunchResult.succeeded();
    } else {
      // 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...');
Devon Carew's avatar
Devon Carew committed
287

Devon Carew's avatar
Devon Carew committed
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
      try {
        Future<List<int>> scrapeServicePorts = Future.wait(
          <Future<int>>[observatoryDiscovery.nextPort(), diagnosticDiscovery.nextPort()]
        );
        List<int> devicePorts = await scrapeServicePorts.timeout(new Duration(seconds: 20));
        int observatoryDevicePort = devicePorts[0];
        printTrace('observatory port = $observatoryDevicePort');
        int observatoryLocalPort = await options.findBestObservatoryPort();
        // TODO(devoncarew): Remember the forwarding information (so we can later remove the
        // port forwarding).
        await _forwardPort(ServiceProtocolDiscovery.kObservatoryService,
            observatoryDevicePort, observatoryLocalPort);
        int diagnosticDevicePort = devicePorts[1];
        printTrace('diagnostic port = $diagnosticDevicePort');
        int diagnosticLocalPort = await options.findBestDiagnosticPort();
        await _forwardPort(ServiceProtocolDiscovery.kDiagnosticService,
            diagnosticDevicePort, diagnosticLocalPort);
        return new LaunchResult.succeeded(
          observatoryPort: observatoryLocalPort,
          diagnosticPort: diagnosticLocalPort
        );
      } 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 new LaunchResult.failed();
      } finally {
        observatoryDiscovery.cancel();
        diagnosticDiscovery.cancel();
      }
Devon Carew's avatar
Devon Carew committed
319
    }
320 321 322
  }

  @override
Devon Carew's avatar
Devon Carew committed
323
  Future<LaunchResult> startApp(
324
    ApplicationPackage package, {
325 326
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
327
    DebuggingOptions debuggingOptions,
328
    Map<String, dynamic> platformArgs
329
  }) async {
330
    if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion())
Devon Carew's avatar
Devon Carew committed
331
      return new LaunchResult.failed();
332

Devon Carew's avatar
Devon Carew committed
333
    String localBundlePath = await flx.buildFlx(
334 335
      mainPath: mainPath,
      includeRobotoFonts: false
336
    );
337 338 339

    if (localBundlePath == null)
      return new LaunchResult.failed();
340 341 342

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

Devon Carew's avatar
Devon Carew committed
343
    return startBundle(
Devon Carew's avatar
Devon Carew committed
344 345 346 347
      package,
      localBundlePath,
      traceStartup: platformArgs['trace-startup'],
      route: route,
Devon Carew's avatar
Devon Carew committed
348 349
      options: debuggingOptions
    );
350 351
  }

352
  @override
353 354 355
  Future<bool> stopApp(ApplicationPackage app) {
    List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]);
    return runCommandAndStreamOutput(command).then((int exitCode) => exitCode == 0);
356 357
  }

358
  @override
359
  void clearLogs() {
360
    runSync(adbCommandForDevice(<String>['logcat', '-c']));
361 362
  }

363
  @override
364 365 366 367 368
  DeviceLogReader get logReader {
    if (_logReader == null)
      _logReader = new _AdbLogReader(this);
    return _logReader;
  }
369

370
  @override
371 372 373 374 375 376 377
  DevicePortForwarder get portForwarder {
    if (_portForwarder == null)
      _portForwarder = new _AndroidDevicePortForwarder(this);

    return _portForwarder;
  }

378 379
  /// 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.
380
  String get lastLogcatTimestamp {
381
    String output = runCheckedSync(adbCommandForDevice(<String>[
382
      'logcat', '-v', 'time', '-t', '1'
383
    ]));
384 385 386

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

390
  @override
391 392
  bool isSupported() => true;

393
  Future<bool> refreshSnapshot(String activity, String snapshotPath) async {
394 395 396 397 398 399 400 401 402 403 404 405 406
    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,
407
      activity,
408 409 410 411
    ]);
    runCheckedSync(cmd);
    return true;
  }
Devon Carew's avatar
Devon Carew committed
412 413 414 415 416 417 418 419 420 421 422 423 424 425

  @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);
  }
426
}
427

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

431 432 433 434
/// Return the list of connected ADB devices.
///
/// [mockAdbOutput] is public for testing.
List<AndroidDevice> getAdbDevices({ String mockAdbOutput }) {
435
  List<AndroidDevice> devices = <AndroidDevice>[];
436 437 438 439 440 441 442 443 444 445
  List<String> output;

  if (mockAdbOutput == null) {
    String adbPath = getAdbPath(androidSdk);
    if (adbPath == null)
      return <AndroidDevice>[];
    output = runSync(<String>[adbPath, 'devices', '-l']).trim().split('\n');
  } else {
    output = mockAdbOutput.trim().split('\n');
  }
446

447 448
  for (String line in output) {
    // Skip lines like: * daemon started successfully *
449 450 451 452 453 454
    if (line.startsWith('* daemon '))
      continue;

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

455 456 457
    if (_kDeviceRegex.hasMatch(line)) {
      Match match = _kDeviceRegex.firstMatch(line);

458
      String deviceID = match[1];
459
      String deviceState = match[2];
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
      String rest = match[3];

      Map<String, String> info = <String, String>{};
      if (rest != null && rest.isNotEmpty) {
        rest = rest.trim();
        for (String data in rest.split(' ')) {
          if (data.contains(':')) {
            List<String> fields = data.split(':');
            info[fields[0]] = fields[1];
          }
        }
      }

      if (info['model'] != null)
        info['model'] = cleanAdbDeviceName(info['model']);
475 476 477 478 479 480 481 482 483

      if (deviceState == 'unauthorized') {
        printError(
          'Device $deviceID is not authorized.\n'
          'You might need to check your device for an authorization dialog.'
        );
      } else if (deviceState == 'offline') {
        printError('Device $deviceID is offline.');
      } else {
484 485 486 487 488 489
        devices.add(new AndroidDevice(
          deviceID,
          productID: info['product'],
          modelID: info['model'] ?? deviceID,
          deviceCodeName: info['device']
        ));
490
      }
491
    } else {
492
      printError(
493 494 495 496 497
        'Unexpected failure parsing device information from adb output:\n'
        '$line\n'
        'Please report a bug at https://github.com/flutter/flutter/issues/new');
    }
  }
498

499 500 501
  return devices;
}

502
/// A log reader that logs from `adb logcat`.
Devon Carew's avatar
Devon Carew committed
503
class _AdbLogReader extends DeviceLogReader {
Devon Carew's avatar
Devon Carew committed
504 505 506 507 508 509
  _AdbLogReader(this.device) {
    _linesController = new StreamController<String>.broadcast(
      onListen: _start,
      onCancel: _stop
    );
  }
Devon Carew's avatar
Devon Carew committed
510 511 512

  final AndroidDevice device;

Devon Carew's avatar
Devon Carew committed
513
  StreamController<String> _linesController;
514 515
  Process _process;

516
  @override
Devon Carew's avatar
Devon Carew committed
517
  Stream<String> get logLines => _linesController.stream;
518

519
  @override
520
  String get name => device.name;
Devon Carew's avatar
Devon Carew committed
521

Devon Carew's avatar
Devon Carew committed
522
  void _start() {
523
    // Start the adb logcat process.
524
    List<String> args = <String>['logcat', '-v', 'tag'];
525 526 527
    String lastTimestamp = device.lastLogcatTimestamp;
    if (lastTimestamp != null)
      args.addAll(<String>['-T', lastTimestamp]);
Devon Carew's avatar
Devon Carew committed
528
    args.addAll(<String>[
Adam Barth's avatar
Adam Barth committed
529
      '-s', 'flutter:V', 'FlutterMain:V', 'FlutterView:V', 'AndroidRuntime:W', 'ActivityManager:W', 'System.err:W', '*:F'
Devon Carew's avatar
Devon Carew committed
530
    ]);
Devon Carew's avatar
Devon Carew committed
531 532 533 534 535 536 537 538 539 540
    runCommand(device.adbCommandForDevice(args)).then((Process process) {
      _process = process;
      _process.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
      _process.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);

      _process.exitCode.then((int code) {
        if (_linesController.hasListener)
          _linesController.close();
      });
    });
541 542 543
  }

  void _onLine(String line) {
544 545 546
    // Filter out some noisy ActivityManager notifications.
    if (line.startsWith('W/ActivityManager: getRunningAppProcesses'))
      return;
Devon Carew's avatar
Devon Carew committed
547
    _linesController.add(line);
Devon Carew's avatar
Devon Carew committed
548 549
  }

Devon Carew's avatar
Devon Carew committed
550 551
  void _stop() {
    // TODO(devoncarew): We should remove adb port forwarding here.
Devon Carew's avatar
Devon Carew committed
552

Devon Carew's avatar
Devon Carew committed
553
    _process?.kill();
Devon Carew's avatar
Devon Carew committed
554 555
  }
}
556 557 558 559 560 561 562 563 564 565

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

  final AndroidDevice device;

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

566
  @override
567 568 569
  List<ForwardedPort> get forwardedPorts {
    final List<ForwardedPort> ports = <ForwardedPort>[];

570 571 572
    String stdout = runCheckedSync(device.adbCommandForDevice(
      <String>['forward', '--list']
    ));
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597

    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;
  }

598
  @override
599
  Future<int> forward(int devicePort, { int hostPort }) async {
600 601 602 603 604
    if ((hostPort == null) || (hostPort == 0)) {
      // Auto select host port.
      hostPort = await findAvailablePort();
    }

605 606 607
    runCheckedSync(device.adbCommandForDevice(
      <String>['forward', 'tcp:$hostPort', 'tcp:$devicePort']
    ));
608 609 610 611

    return hostPort;
  }

612
  @override
Ian Hickson's avatar
Ian Hickson committed
613
  Future<Null> unforward(ForwardedPort forwardedPort) async {
614 615 616
    runCheckedSync(device.adbCommandForDevice(
      <String>['forward', '--remove', 'tcp:${forwardedPort.hostPort}']
    ));
617 618
  }
}