| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:io'; |
| |
| import 'package:crypto/crypto.dart'; |
| import 'package:path/path.dart' as path; |
| |
| import 'application_package.dart'; |
| import 'base/logging.dart'; |
| import 'base/process.dart'; |
| import 'build_configuration.dart'; |
| |
| abstract class Device { |
| final String id; |
| static Map<String, Device> _deviceCache = {}; |
| |
| static Device _unique(String id, Device constructor(String id)) { |
| return _deviceCache.putIfAbsent(id, () => constructor(id)); |
| } |
| |
| Device._(this.id); |
| |
| /// Install an app package on the current device |
| bool installApp(ApplicationPackage app); |
| |
| /// Check if the device is currently connected |
| bool isConnected(); |
| |
| /// Check if the current version of the given app is already installed |
| bool isAppInstalled(ApplicationPackage app); |
| |
| TargetPlatform get platform; |
| |
| Future<int> logs({bool clear: false}); |
| |
| /// Start an app package on the current device |
| Future<bool> startApp(ApplicationPackage app); |
| |
| /// Stop an app package on the current device |
| Future<bool> stopApp(ApplicationPackage app); |
| |
| String toString() => '$runtimeType $id'; |
| } |
| |
| class IOSDevice extends Device { |
| static final String defaultDeviceID = 'default_ios_id'; |
| |
| static const String _macInstructions = |
| 'To work with iOS devices, please install ideviceinstaller. ' |
| 'If you use homebrew, you can install it with ' |
| '"\$ brew install ideviceinstaller".'; |
| static const String _linuxInstructions = |
| 'To work with iOS devices, please install ideviceinstaller. ' |
| 'On Ubuntu or Debian, you can install it with ' |
| '"\$ apt-get install ideviceinstaller".'; |
| |
| 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; |
| |
| String _name; |
| String get name => _name; |
| |
| factory IOSDevice({String id, String name}) { |
| IOSDevice device = Device._unique(id ?? defaultDeviceID, (String id) => new IOSDevice._(id)); |
| device._name = name; |
| return device; |
| } |
| |
| IOSDevice._(String id) : 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', |
| 'Copying files to iOS devices is not currently supported on Linux.'); |
| } |
| |
| static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) { |
| List<IOSDevice> devices = []; |
| for (String id in _getAttachedDeviceIDs(mockIOS)) { |
| String name = _getDeviceName(id, mockIOS); |
| devices.add(new IOSDevice(id: id, name: name)); |
| } |
| return devices; |
| } |
| |
| static Iterable<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) { |
| String listerPath = |
| (mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id'); |
| String output; |
| try { |
| output = runSync([listerPath, '-l']); |
| } catch (e) { |
| return []; |
| } |
| return output.trim() |
| .split('\n') |
| .where((String s) => s != null && s.length > 0); |
| } |
| |
| static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) { |
| String informerPath = (mockIOS != null) |
| ? mockIOS.informerPath |
| : _checkForCommand('ideviceinfo'); |
| return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]); |
| } |
| |
| static final Map<String, String> _commandMap = {}; |
| static String _checkForCommand(String command, |
| [String macInstructions = _macInstructions, |
| String linuxInstructions = _linuxInstructions]) { |
| return _commandMap.putIfAbsent(command, () { |
| try { |
| command = runCheckedSync(['which', command]).trim(); |
| } catch (e) { |
| if (Platform.isMacOS) { |
| logging.severe(macInstructions); |
| } else if (Platform.isLinux) { |
| logging.severe(linuxInstructions); |
| } else { |
| logging.severe('$command is not available on your platform.'); |
| } |
| } |
| return command; |
| }); |
| } |
| |
| @override |
| bool installApp(ApplicationPackage app) { |
| try { |
| if (id == defaultDeviceID) { |
| runCheckedSync([installerPath, '-i', app.localPath]); |
| } else { |
| runCheckedSync([installerPath, '-u', id, '-i', app.localPath]); |
| } |
| return true; |
| } catch (e) { |
| return false; |
| } |
| return false; |
| } |
| |
| @override |
| bool isConnected() { |
| Iterable<String> ids = _getAttachedDeviceIDs(); |
| for (String id in ids) { |
| if (id == this.id || this.id == defaultDeviceID) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @override |
| bool isAppInstalled(ApplicationPackage app) { |
| try { |
| String apps = runCheckedSync([installerPath, '-l']); |
| if (new RegExp(app.id, multiLine: true).hasMatch(apps)) { |
| return true; |
| } |
| } catch (e) { |
| return false; |
| } |
| return false; |
| } |
| |
| @override |
| Future<bool> startApp(ApplicationPackage app) async { |
| if (!isAppInstalled(app)) { |
| return false; |
| } |
| // idevicedebug hangs forever after launching the app, so kill it after |
| // giving it plenty of time to send the launch command. |
| return await runAndKill( |
| [debuggerPath, 'run', app.id], |
| new Duration(seconds: 3) |
| ).then( |
| (_) { |
| return true; |
| }, onError: (e) { |
| logging.info('Failure running $debuggerPath: ', e); |
| return false; |
| } |
| ); |
| } |
| |
| @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([ |
| pusherPath, |
| '-t', |
| '1', |
| '--bundle_id', |
| app.id, |
| '--upload', |
| localFile, |
| '--to', |
| targetFile |
| ]); |
| return true; |
| } else { |
| // TODO(iansf): It may be possible to make this work on Linux. Since this |
| // functionality appears to be the only that prevents us from |
| // supporting iOS on Linux, it may be worth putting some time |
| // into investigating this. |
| // See https://bbs.archlinux.org/viewtopic.php?id=192655 |
| return false; |
| } |
| return false; |
| } |
| |
| @override |
| TargetPlatform get platform => TargetPlatform.iOS; |
| |
| /// Note that clear is not supported on iOS at this time. |
| Future<int> logs({bool clear: false}) async { |
| if (!isConnected()) { |
| return 2; |
| } |
| return await runCommandAndStreamOutput([loggerPath], |
| prefix: 'iOS dev: ', filter: new RegExp(r'.*SkyShell.*')); |
| } |
| } |
| |
| class IOSSimulator extends Device { |
| static final String defaultDeviceID = 'default_ios_sim_id'; |
| |
| static const String _macInstructions = |
| 'To work with iOS devices, please install ideviceinstaller. ' |
| 'If you use homebrew, you can install it with ' |
| '"\$ brew install ideviceinstaller".'; |
| |
| static String _xcrunPath = path.join('/usr', 'bin', 'xcrun'); |
| |
| String _iOSSimPath; |
| String get iOSSimPath => _iOSSimPath; |
| |
| String get xcrunPath => _xcrunPath; |
| |
| String _name; |
| String get name => _name; |
| |
| factory IOSSimulator({String id, String name, String iOSSimulatorPath}) { |
| IOSSimulator device = Device._unique(id ?? defaultDeviceID, (String id) => new IOSSimulator._(id)); |
| device._name = name; |
| if (iOSSimulatorPath == null) { |
| iOSSimulatorPath = path.join('/Applications', 'iOS Simulator.app', |
| 'Contents', 'MacOS', 'iOS Simulator'); |
| } |
| device._iOSSimPath = iOSSimulatorPath; |
| return device; |
| } |
| |
| IOSSimulator._(String id) : super._(id); |
| |
| static String _getRunningSimulatorID([IOSSimulator mockIOS]) { |
| String xcrunPath = mockIOS != null ? mockIOS.xcrunPath : _xcrunPath; |
| String output = runCheckedSync([xcrunPath, 'simctl', 'list', 'devices']); |
| |
| Match match; |
| Iterable<Match> matches = new RegExp(r'[^\(]+\(([^\)]+)\) \(Booted\)', |
| multiLine: true).allMatches(output); |
| if (matches.length > 1) { |
| // More than one simulator is listed as booted, which is not allowed but |
| // sometimes happens erroneously. Kill them all because we don't know |
| // which one is actually running. |
| logging.warning('Multiple running simulators were detected, ' |
| 'which is not supposed to happen.'); |
| for (Match m in matches) { |
| if (m.groupCount > 0) { |
| logging.warning('Killing simulator ${m.group(1)}'); |
| runSync([xcrunPath, 'simctl', 'shutdown', m.group(1)]); |
| } |
| } |
| } else if (matches.length == 1) { |
| match = matches.first; |
| } |
| |
| if (match != null && match.groupCount > 0) { |
| return match.group(1); |
| } else { |
| logging.info('No running simulators found'); |
| return null; |
| } |
| } |
| |
| String _getSimulatorPath() { |
| String deviceID = id == defaultDeviceID ? _getRunningSimulatorID() : id; |
| String homeDirectory = path.absolute(Platform.environment['HOME']); |
| if (deviceID == null) { |
| return null; |
| } |
| return path.join(homeDirectory, 'Library', 'Developer', 'CoreSimulator', |
| 'Devices', deviceID); |
| } |
| |
| String _getSimulatorAppHomeDirectory(ApplicationPackage app) { |
| String simulatorPath = _getSimulatorPath(); |
| if (simulatorPath == null) { |
| return null; |
| } |
| return path.join(simulatorPath, 'data'); |
| } |
| |
| static List<IOSSimulator> getAttachedDevices([IOSSimulator mockIOS]) { |
| List<IOSSimulator> devices = []; |
| String id = _getRunningSimulatorID(mockIOS); |
| if (id != null) { |
| // TODO(iansf): get the simulator's name |
| // String name = _getDeviceName(id, mockIOS); |
| devices.add(new IOSSimulator(id: id)); |
| } |
| return devices; |
| } |
| |
| Future<bool> boot() async { |
| if (!Platform.isMacOS) { |
| return false; |
| } |
| if (isConnected()) { |
| return true; |
| } |
| if (id == defaultDeviceID) { |
| runDetached([iOSSimPath]); |
| Future<bool> checkConnection([int attempts = 20]) async { |
| if (attempts == 0) { |
| logging.info('Timed out waiting for iOS Simulator $id to boot.'); |
| return false; |
| } |
| if (!isConnected()) { |
| logging.info('Waiting for iOS Simulator $id 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', id]); |
| } catch (e) { |
| logging.warning('Unable to boot iOS Simulator $id: ', e); |
| return false; |
| } |
| } |
| return false; |
| } |
| |
| @override |
| bool installApp(ApplicationPackage app) { |
| if (!isConnected()) { |
| return false; |
| } |
| try { |
| if (id == defaultDeviceID) { |
| runCheckedSync([xcrunPath, 'simctl', 'install', 'booted', app.localPath]); |
| } else { |
| runCheckedSync([xcrunPath, 'simctl', 'install', id, app.localPath]); |
| } |
| return true; |
| } catch (e) { |
| return false; |
| } |
| } |
| |
| @override |
| bool isConnected() { |
| if (!Platform.isMacOS) { |
| return false; |
| } |
| String simulatorID = _getRunningSimulatorID(); |
| if (simulatorID == null) { |
| return false; |
| } else if (id == defaultDeviceID) { |
| return true; |
| } else { |
| return _getRunningSimulatorID() == id; |
| } |
| } |
| |
| @override |
| bool isAppInstalled(ApplicationPackage app) { |
| try { |
| String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app); |
| return FileSystemEntity.isDirectorySync(simulatorHomeDirectory); |
| } catch (e) { |
| return false; |
| } |
| } |
| |
| @override |
| Future<bool> startApp(ApplicationPackage app) async { |
| if (!isAppInstalled(app)) { |
| return false; |
| } |
| try { |
| if (id == defaultDeviceID) { |
| runCheckedSync( |
| [xcrunPath, 'simctl', 'launch', 'booted', app.id]); |
| } else { |
| runCheckedSync([xcrunPath, 'simctl', 'launch', id, app.id]); |
| } |
| return true; |
| } catch (e) { |
| return false; |
| } |
| } |
| |
| @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( |
| ['cp', localFile, path.join(simulatorHomeDirectory, targetFile)]); |
| return true; |
| } |
| return false; |
| } |
| |
| @override |
| TargetPlatform get platform => TargetPlatform.iOSSimulator; |
| |
| Future<int> logs({bool clear: false}) async { |
| if (!isConnected()) { |
| return 2; |
| } |
| String homeDirectory = path.absolute(Platform.environment['HOME']); |
| String simulatorDeviceID = _getRunningSimulatorID(); |
| String logFilePath = path.join(homeDirectory, 'Library', 'Logs', |
| 'CoreSimulator', simulatorDeviceID, 'system.log'); |
| if (clear) { |
| runSync(['rm', logFilePath]); |
| } |
| return await runCommandAndStreamOutput(['tail', '-f', logFilePath], |
| prefix: 'iOS sim: ', filter: new RegExp(r'.*SkyShell.*')); |
| } |
| } |
| |
| class AndroidDevice extends Device { |
| static const String _ADB_PATH = 'adb'; |
| static const int _observatoryPort = 8181; |
| |
| static final String defaultDeviceID = 'default_android_device'; |
| |
| String productID; |
| String modelID; |
| String deviceCodeName; |
| |
| String _adbPath; |
| String get adbPath => _adbPath; |
| bool _hasAdb = false; |
| bool _hasValidAndroid = false; |
| |
| factory AndroidDevice( |
| {String id: null, |
| String productID: null, |
| String modelID: null, |
| String deviceCodeName: null}) { |
| AndroidDevice device = Device._unique(id ?? defaultDeviceID, (String id) => new AndroidDevice._(id)); |
| device.productID = productID; |
| device.modelID = modelID; |
| device.deviceCodeName = deviceCodeName; |
| return device; |
| } |
| |
| /// mockAndroid argument is only to facilitate testing with mocks, so that |
| /// we don't have to rely on the test setup having adb available to it. |
| static List<AndroidDevice> getAttachedDevices([AndroidDevice mockAndroid]) { |
| List<AndroidDevice> devices = []; |
| String adbPath = (mockAndroid != null) ? mockAndroid.adbPath : _getAdbPath(); |
| |
| try { |
| runCheckedSync([adbPath, 'version']); |
| } catch (e) { |
| logging.severe('Unable to find adb. Is "adb" in your path?'); |
| return devices; |
| } |
| |
| List<String> output = runSync([adbPath, 'devices', '-l']).trim().split('\n'); |
| |
| // 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+$'); |
| |
| // Skip first line, which is always 'List of devices attached'. |
| 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 (deviceRegex1.hasMatch(line)) { |
| Match match = deviceRegex1.firstMatch(line); |
| String deviceID = match[1]; |
| String productID = match[2]; |
| String modelID = match[3]; |
| String deviceCodeName = match[4]; |
| |
| devices.add(new AndroidDevice( |
| id: deviceID, |
| productID: productID, |
| modelID: modelID, |
| deviceCodeName: deviceCodeName |
| )); |
| } else if (deviceRegex2.hasMatch(line)) { |
| Match match = deviceRegex2.firstMatch(line); |
| String deviceID = match[1]; |
| devices.add(new AndroidDevice(id: deviceID)); |
| } else if (unauthorizedRegex.hasMatch(line)) { |
| Match match = unauthorizedRegex.firstMatch(line); |
| String deviceID = match[1]; |
| logging.warning( |
| '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]; |
| logging.warning('Device $deviceID is offline.'); |
| } else { |
| logging.warning( |
| '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; |
| } |
| |
| AndroidDevice._(id) : super._(id) { |
| _adbPath = _getAdbPath(); |
| _hasAdb = _checkForAdb(); |
| |
| // Checking for Jelly Bean only needs to be done if we are starting an |
| // app, but it has an important side effect, which is to discard any |
| // progress messages if the adb server is restarted. |
| _hasValidAndroid = _checkForSupportedAndroidVersion(); |
| |
| if (!_hasAdb || !_hasValidAndroid) { |
| logging.warning('Unable to run on Android.'); |
| } |
| } |
| |
| static String getAndroidSdkPath() { |
| if (Platform.environment.containsKey('ANDROID_HOME')) { |
| String androidHomeDir = Platform.environment['ANDROID_HOME']; |
| if (FileSystemEntity.isDirectorySync( |
| path.join(androidHomeDir, 'platform-tools'))) { |
| return androidHomeDir; |
| } else if (FileSystemEntity.isDirectorySync( |
| path.join(androidHomeDir, 'sdk', 'platform-tools'))) { |
| return path.join(androidHomeDir, 'sdk'); |
| } else { |
| logging.warning('Android SDK not found at $androidHomeDir'); |
| return null; |
| } |
| } else { |
| logging.warning('Android SDK not found. The ANDROID_HOME variable must be set.'); |
| return null; |
| } |
| } |
| |
| static String _getAdbPath() { |
| if (Platform.environment.containsKey('ANDROID_HOME')) { |
| String androidHomeDir = Platform.environment['ANDROID_HOME']; |
| String adbPath1 = |
| path.join(androidHomeDir, 'sdk', 'platform-tools', 'adb'); |
| String adbPath2 = path.join(androidHomeDir, 'platform-tools', 'adb'); |
| if (FileSystemEntity.isFileSync(adbPath1)) { |
| return adbPath1; |
| } else if (FileSystemEntity.isFileSync(adbPath2)) { |
| return adbPath2; |
| } else { |
| logging.info('"adb" not found at\n "$adbPath1" or\n "$adbPath2"\n' + |
| 'using default path "$_ADB_PATH"'); |
| return _ADB_PATH; |
| } |
| } else { |
| return _ADB_PATH; |
| } |
| } |
| |
| List<String> adbCommandForDevice(List<String> args) { |
| List<String> result = <String>[adbPath]; |
| if (id != defaultDeviceID) { |
| result.addAll(['-s', id]); |
| } |
| result.addAll(args); |
| return result; |
| } |
| |
| bool _isValidAdbVersion(String adbVersion) { |
| // Sample output: 'Android Debug Bridge version 1.0.31' |
| Match versionFields = |
| new RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion); |
| 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; |
| } |
| logging.warning( |
| 'Unrecognized adb version string $adbVersion. Skipping version check.'); |
| return true; |
| } |
| |
| bool _checkForAdb() { |
| try { |
| String adbVersion = runCheckedSync([adbPath, 'version']); |
| if (_isValidAdbVersion(adbVersion)) { |
| return true; |
| } |
| |
| String locatedAdbPath = runCheckedSync(['which', 'adb']); |
| logging.severe('"$locatedAdbPath" is too old. ' |
| 'Please install version 1.0.32 or later.\n' |
| 'Try setting ANDROID_HOME to the path to your Android SDK install. ' |
| 'Android builds are unavailable.'); |
| } catch (e, stack) { |
| logging.severe('"adb" not found in \$PATH. ' |
| 'Please install the Android SDK or set ANDROID_HOME ' |
| 'to the path of your Android SDK install.'); |
| logging.info(e); |
| logging.info(stack); |
| } |
| 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 * |
| runCheckedSync(adbCommandForDevice(['start-server'])); |
| |
| String ready = runSync(adbCommandForDevice(['shell', 'echo', 'ready'])); |
| if (ready.trim() != 'ready') { |
| logging.info('Android device not found.'); |
| return false; |
| } |
| |
| // Sample output: '22' |
| String sdkVersion = |
| runCheckedSync(adbCommandForDevice(['shell', 'getprop', 'ro.build.version.sdk'])) |
| .trimRight(); |
| |
| int sdkVersionParsed = |
| int.parse(sdkVersion, onError: (String source) => null); |
| if (sdkVersionParsed == null) { |
| logging.severe('Unexpected response from getprop: "$sdkVersion"'); |
| return false; |
| } |
| if (sdkVersionParsed < 16) { |
| logging.severe('The Android version ($sdkVersion) on the target device ' |
| 'is too old. Please use a Jelly Bean (version 16 / 4.1.x) device or later.'); |
| return false; |
| } |
| return true; |
| } catch (e) { |
| logging.severe('Unexpected failure from adb: ', e); |
| } |
| return false; |
| } |
| |
| String _getDeviceSha1Path(ApplicationPackage app) { |
| return '/data/local/tmp/sky.${app.id}.sha1'; |
| } |
| |
| String _getDeviceApkSha1(ApplicationPackage app) { |
| return runCheckedSync(adbCommandForDevice(['shell', 'cat', _getDeviceSha1Path(app)])); |
| } |
| |
| String _getSourceSha1(ApplicationPackage app) { |
| var sha1 = new SHA1(); |
| var file = new File(app.localPath); |
| sha1.add(file.readAsBytesSync()); |
| return CryptoUtils.bytesToHex(sha1.close()); |
| } |
| |
| @override |
| bool isAppInstalled(ApplicationPackage app) { |
| if (!isConnected()) { |
| return false; |
| } |
| if (runCheckedSync(adbCommandForDevice(['shell', 'pm', 'path', app.id])) == |
| '') { |
| logging.info( |
| 'TODO(iansf): move this log to the caller. ${app.name} is not on the device. Installing now...'); |
| return false; |
| } |
| if (_getDeviceApkSha1(app) != _getSourceSha1(app)) { |
| logging.info( |
| 'TODO(iansf): move this log to the caller. ${app.name} is out of date. Installing now...'); |
| return false; |
| } |
| return true; |
| } |
| |
| @override |
| bool installApp(ApplicationPackage app) { |
| if (!isConnected()) { |
| logging.info('Android device not connected. Not installing.'); |
| return false; |
| } |
| if (!FileSystemEntity.isFileSync(app.localPath)) { |
| logging.severe('"${app.localPath}" does not exist.'); |
| return false; |
| } |
| |
| print('Installing ${app.name} on device.'); |
| runCheckedSync(adbCommandForDevice(['install', '-r', app.localPath])); |
| runCheckedSync(adbCommandForDevice(['shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app)])); |
| return true; |
| } |
| |
| void _forwardObservatoryPort() { |
| // Set up port forwarding for observatory. |
| String portString = 'tcp:$_observatoryPort'; |
| runCheckedSync(adbCommandForDevice(['forward', portString, portString])); |
| } |
| |
| bool startBundle(AndroidApk apk, String bundlePath, { |
| bool poke, |
| bool checked, |
| bool traceStartup, |
| String route |
| }) { |
| logging.fine('$this startBundle'); |
| |
| if (!FileSystemEntity.isFileSync(bundlePath)) { |
| logging.severe('Cannot find $bundlePath'); |
| return false; |
| } |
| |
| if (!poke) |
| _forwardObservatoryPort(); |
| |
| String deviceTmpPath = '/data/local/tmp/dev.flx'; |
| runCheckedSync(adbCommandForDevice(['push', bundlePath, deviceTmpPath])); |
| List<String> cmd = adbCommandForDevice([ |
| 'shell', 'am', 'start', |
| '-a', 'android.intent.action.RUN', |
| '-d', deviceTmpPath, |
| ]); |
| if (checked) |
| cmd.addAll(['--ez', 'enable-checked-mode', 'true']); |
| if (traceStartup) |
| cmd.addAll(['--ez', 'trace-startup', 'true']); |
| if (route != null) |
| cmd.addAll(['--es', 'route', route]); |
| cmd.add(apk.launchActivity); |
| runCheckedSync(cmd); |
| return true; |
| } |
| |
| @override |
| Future<bool> startApp(ApplicationPackage app) async { |
| // Android currently has to be started with startBundle(...). |
| assert(false); |
| return false; |
| } |
| |
| Future<bool> stopApp(ApplicationPackage app) async { |
| final AndroidApk apk = app; |
| runSync(adbCommandForDevice(['shell', 'am', 'force-stop', apk.id])); |
| return true; |
| } |
| |
| @override |
| TargetPlatform get platform => TargetPlatform.android; |
| |
| void clearLogs() { |
| runSync(adbCommandForDevice(['logcat', '-c'])); |
| } |
| |
| Future<int> logs({bool clear: false}) async { |
| if (!isConnected()) { |
| return 2; |
| } |
| |
| if (clear) { |
| clearLogs(); |
| } |
| |
| return await runCommandAndStreamOutput(adbCommandForDevice([ |
| 'logcat', |
| '-v', |
| 'tag', // Only log the tag and the message |
| '-s', |
| 'flutter:V', |
| 'chromium:D', |
| 'ActivityManager:W', |
| '*:F', |
| ]), prefix: 'android: '); |
| } |
| |
| void startTracing(AndroidApk apk) { |
| runCheckedSync(adbCommandForDevice([ |
| 'shell', |
| 'am', |
| 'broadcast', |
| '-a', |
| '${apk.id}.TRACING_START' |
| ])); |
| } |
| |
| static String _threeDigits(int n) { |
| if (n >= 100) return "$n"; |
| if (n >= 10) return "0$n"; |
| return "00$n"; |
| } |
| |
| static String _twoDigits(int n) { |
| if (n >= 10) return "$n"; |
| return "0$n"; |
| } |
| |
| static String _logcatDateFormat(DateTime dt) { |
| // Doing this manually, instead of using package:intl for simplicity. |
| // adb logcat -T wants "%m-%d %H:%M:%S.%3q" |
| String m = _twoDigits(dt.month); |
| String d = _twoDigits(dt.day); |
| String H = _twoDigits(dt.hour); |
| String M = _twoDigits(dt.minute); |
| String S = _twoDigits(dt.second); |
| String q = _threeDigits(dt.millisecond); |
| return "$m-$d $H:$M:$S.$q"; |
| } |
| |
| String stopTracing(AndroidApk apk, { String outPath: null }) { |
| // Workaround for logcat -c not always working: |
| // http://stackoverflow.com/questions/25645012/logcat-on-android-l-not-clearing-after-unplugging-and-reconnecting |
| String beforeStop = _logcatDateFormat(new DateTime.now()); |
| runCheckedSync(adbCommandForDevice([ |
| '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); |
| |
| String tracePath = null; |
| bool isComplete = false; |
| while (!isComplete) { |
| String logs = runCheckedSync(adbCommandForDevice(['logcat', '-d', '-T', beforeStop])); |
| 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); |
| runCheckedSync(adbCommandForDevice(['root'])); |
| runSync(adbCommandForDevice(['shell', 'run-as', apk.id, 'chmod', '777', tracePath])); |
| runCheckedSync(adbCommandForDevice(['pull', tracePath, localPath])); |
| runSync(adbCommandForDevice(['shell', 'rm', tracePath])); |
| return localPath; |
| } |
| logging.warning('No trace file detected. ' |
| 'Did you remember to start the trace before stopping it?'); |
| return null; |
| } |
| |
| @override |
| bool isConnected() => _hasValidAndroid; |
| } |
| |
| class DeviceStore { |
| final AndroidDevice android; |
| final IOSDevice iOS; |
| final IOSSimulator iOSSimulator; |
| |
| List<Device> get all { |
| List<Device> result = <Device>[]; |
| if (android != null) |
| result.add(android); |
| if (iOS != null) |
| result.add(iOS); |
| if (iOSSimulator != null) |
| result.add(iOSSimulator); |
| return result; |
| } |
| |
| DeviceStore({ |
| this.android, |
| this.iOS, |
| this.iOSSimulator |
| }); |
| |
| factory DeviceStore.forConfigs(List<BuildConfiguration> configs) { |
| AndroidDevice android; |
| IOSDevice iOS; |
| IOSSimulator iOSSimulator; |
| |
| for (BuildConfiguration config in configs) { |
| switch (config.targetPlatform) { |
| case TargetPlatform.android: |
| assert(android == null); |
| List<AndroidDevice> androidDevices = AndroidDevice.getAttachedDevices(); |
| if (config.deviceId != null) { |
| android = androidDevices.firstWhere( |
| (AndroidDevice dev) => (dev.id == config.deviceId), |
| orElse: () => null); |
| if (android == null) { |
| print('Warning: Device ID ${config.deviceId} not found'); |
| } |
| } else if (androidDevices.length == 1) { |
| android = androidDevices[0]; |
| } else if (androidDevices.length > 1) { |
| print('Warning: Multiple Android devices are connected, but no device ID was specified.'); |
| } |
| break; |
| case TargetPlatform.iOS: |
| assert(iOS == null); |
| iOS = new IOSDevice(); |
| break; |
| case TargetPlatform.iOSSimulator: |
| assert(iOSSimulator == null); |
| iOSSimulator = new IOSSimulator(); |
| break; |
| case TargetPlatform.mac: |
| case TargetPlatform.linux: |
| break; |
| } |
| } |
| |
| return new DeviceStore(android: android, iOS: iOS, iOSSimulator: iOSSimulator); |
| } |
| } |