|
|
|
@ -2,8 +2,6 @@
|
|
|
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
|
|
|
// found in the LICENSE file.
|
|
|
|
|
|
|
|
|
|
// @dart = 2.8
|
|
|
|
|
|
|
|
|
|
import 'dart:async';
|
|
|
|
|
|
|
|
|
|
import 'package:meta/meta.dart';
|
|
|
|
@ -60,13 +58,13 @@ class AndroidDevice extends Device {
|
|
|
|
|
AndroidDevice(
|
|
|
|
|
String id, {
|
|
|
|
|
this.productID,
|
|
|
|
|
this.modelID,
|
|
|
|
|
required this.modelID,
|
|
|
|
|
this.deviceCodeName,
|
|
|
|
|
@required Logger logger,
|
|
|
|
|
@required ProcessManager processManager,
|
|
|
|
|
@required Platform platform,
|
|
|
|
|
@required AndroidSdk androidSdk,
|
|
|
|
|
@required FileSystem fileSystem,
|
|
|
|
|
required Logger logger,
|
|
|
|
|
required ProcessManager processManager,
|
|
|
|
|
required Platform platform,
|
|
|
|
|
required AndroidSdk androidSdk,
|
|
|
|
|
required FileSystem fileSystem,
|
|
|
|
|
AndroidConsoleSocketFactory androidConsoleSocketFactory = kAndroidConsoleSocketFactory,
|
|
|
|
|
}) : _logger = logger,
|
|
|
|
|
_processManager = processManager,
|
|
|
|
@ -90,60 +88,53 @@ class AndroidDevice extends Device {
|
|
|
|
|
final ProcessUtils _processUtils;
|
|
|
|
|
final AndroidConsoleSocketFactory _androidConsoleSocketFactory;
|
|
|
|
|
|
|
|
|
|
final String productID;
|
|
|
|
|
final String? productID;
|
|
|
|
|
final String modelID;
|
|
|
|
|
final String deviceCodeName;
|
|
|
|
|
final String? deviceCodeName;
|
|
|
|
|
|
|
|
|
|
Map<String, String> _properties;
|
|
|
|
|
bool _isLocalEmulator;
|
|
|
|
|
TargetPlatform _applicationPlatform;
|
|
|
|
|
late final Future<Map<String, String>> _properties = () async {
|
|
|
|
|
Map<String, String> properties = <String, String>{};
|
|
|
|
|
|
|
|
|
|
Future<String> _getProperty(String name) async {
|
|
|
|
|
if (_properties == null) {
|
|
|
|
|
_properties = <String, String>{};
|
|
|
|
|
final List<String> propCommand = adbCommandForDevice(<String>['shell', 'getprop']);
|
|
|
|
|
_logger.printTrace(propCommand.join(' '));
|
|
|
|
|
|
|
|
|
|
final List<String> propCommand = adbCommandForDevice(<String>['shell', 'getprop']);
|
|
|
|
|
_logger.printTrace(propCommand.join(' '));
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// We pass an encoding of latin1 so that we don't try and interpret the
|
|
|
|
|
// `adb shell getprop` result as UTF8.
|
|
|
|
|
final ProcessResult result = await _processManager.run(
|
|
|
|
|
propCommand,
|
|
|
|
|
stdoutEncoding: latin1,
|
|
|
|
|
stderrEncoding: latin1,
|
|
|
|
|
);
|
|
|
|
|
if (result.exitCode == 0 || _allowHeapCorruptionOnWindows(result.exitCode, _platform)) {
|
|
|
|
|
_properties = parseAdbDeviceProperties(result.stdout as String);
|
|
|
|
|
} else {
|
|
|
|
|
_logger.printError('Error ${result.exitCode} retrieving device properties for $name:');
|
|
|
|
|
_logger.printError(result.stderr as String);
|
|
|
|
|
}
|
|
|
|
|
} on ProcessException catch (error) {
|
|
|
|
|
_logger.printError('Error retrieving device properties for $name: $error');
|
|
|
|
|
try {
|
|
|
|
|
// We pass an encoding of latin1 so that we don't try and interpret the
|
|
|
|
|
// `adb shell getprop` result as UTF8.
|
|
|
|
|
final ProcessResult result = await _processManager.run(
|
|
|
|
|
propCommand,
|
|
|
|
|
stdoutEncoding: latin1,
|
|
|
|
|
stderrEncoding: latin1,
|
|
|
|
|
);
|
|
|
|
|
if (result.exitCode == 0 || _allowHeapCorruptionOnWindows(result.exitCode, _platform)) {
|
|
|
|
|
properties = parseAdbDeviceProperties(result.stdout as String);
|
|
|
|
|
} else {
|
|
|
|
|
_logger.printError('Error ${result.exitCode} retrieving device properties for $name:');
|
|
|
|
|
_logger.printError(result.stderr as String);
|
|
|
|
|
}
|
|
|
|
|
} on ProcessException catch (error) {
|
|
|
|
|
_logger.printError('Error retrieving device properties for $name: $error');
|
|
|
|
|
}
|
|
|
|
|
return properties;
|
|
|
|
|
}();
|
|
|
|
|
|
|
|
|
|
return _properties[name];
|
|
|
|
|
Future<String?> _getProperty(String name) async {
|
|
|
|
|
return (await _properties)[name];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<bool> get isLocalEmulator async {
|
|
|
|
|
if (_isLocalEmulator == null) {
|
|
|
|
|
final String hardware = await _getProperty('ro.hardware');
|
|
|
|
|
_logger.printTrace('ro.hardware = $hardware');
|
|
|
|
|
if (kKnownHardware.containsKey(hardware)) {
|
|
|
|
|
// Look for known hardware models.
|
|
|
|
|
_isLocalEmulator = kKnownHardware[hardware] == HardwareType.emulator;
|
|
|
|
|
} else {
|
|
|
|
|
// Fall back to a best-effort heuristic-based approach.
|
|
|
|
|
final String characteristics = await _getProperty('ro.build.characteristics');
|
|
|
|
|
_logger.printTrace('ro.build.characteristics = $characteristics');
|
|
|
|
|
_isLocalEmulator = characteristics != null && characteristics.contains('emulator');
|
|
|
|
|
}
|
|
|
|
|
late final Future<bool> isLocalEmulator = () async {
|
|
|
|
|
final String? hardware = await _getProperty('ro.hardware');
|
|
|
|
|
_logger.printTrace('ro.hardware = $hardware');
|
|
|
|
|
if (kKnownHardware.containsKey(hardware)) {
|
|
|
|
|
// Look for known hardware models.
|
|
|
|
|
return kKnownHardware[hardware] == HardwareType.emulator;
|
|
|
|
|
}
|
|
|
|
|
return _isLocalEmulator;
|
|
|
|
|
}
|
|
|
|
|
// Fall back to a best-effort heuristic-based approach.
|
|
|
|
|
final String? characteristics = await _getProperty('ro.build.characteristics');
|
|
|
|
|
_logger.printTrace('ro.build.characteristics = $characteristics');
|
|
|
|
|
return characteristics != null && characteristics.contains('emulator');
|
|
|
|
|
}();
|
|
|
|
|
|
|
|
|
|
/// The unique identifier for the emulator that corresponds to this device, or
|
|
|
|
|
/// null if it is not an emulator.
|
|
|
|
@ -152,7 +143,7 @@ class AndroidDevice extends Device {
|
|
|
|
|
/// this name may require connecting to the device and if an error occurs null
|
|
|
|
|
/// will be returned.
|
|
|
|
|
@override
|
|
|
|
|
Future<String> get emulatorId async {
|
|
|
|
|
Future<String?> get emulatorId async {
|
|
|
|
|
if (!(await isLocalEmulator)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
@ -161,13 +152,13 @@ class AndroidDevice extends Device {
|
|
|
|
|
// Android Console port number.
|
|
|
|
|
final RegExp emulatorPortRegex = RegExp(r'emulator-(\d+)');
|
|
|
|
|
|
|
|
|
|
final Match portMatch = emulatorPortRegex.firstMatch(id);
|
|
|
|
|
final Match? portMatch = emulatorPortRegex.firstMatch(id);
|
|
|
|
|
if (portMatch == null || portMatch.groupCount < 1) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const String host = 'localhost';
|
|
|
|
|
final int port = int.parse(portMatch.group(1));
|
|
|
|
|
final int port = int.parse(portMatch.group(1)!);
|
|
|
|
|
_logger.printTrace('Fetching avd name for $name via Android console on $host:$port');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
@ -196,36 +187,28 @@ class AndroidDevice extends Device {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<TargetPlatform> get targetPlatform async {
|
|
|
|
|
if (_applicationPlatform == null) {
|
|
|
|
|
// http://developer.android.com/ndk/guides/abis.html (x86, armeabi-v7a, ...)
|
|
|
|
|
switch (await _getProperty('ro.product.cpu.abi')) {
|
|
|
|
|
case 'arm64-v8a':
|
|
|
|
|
// Perform additional verification for 64 bit ABI. Some devices,
|
|
|
|
|
// like the Kindle Fire 8, misreport the abilist. We might not
|
|
|
|
|
// be able to retrieve this property, in which case we fall back
|
|
|
|
|
// to assuming 64 bit.
|
|
|
|
|
final String abilist = await _getProperty('ro.product.cpu.abilist');
|
|
|
|
|
if (abilist == null || abilist.contains('arm64-v8a')) {
|
|
|
|
|
_applicationPlatform = TargetPlatform.android_arm64;
|
|
|
|
|
} else {
|
|
|
|
|
_applicationPlatform = TargetPlatform.android_arm;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'x86_64':
|
|
|
|
|
_applicationPlatform = TargetPlatform.android_x64;
|
|
|
|
|
break;
|
|
|
|
|
case 'x86':
|
|
|
|
|
_applicationPlatform = TargetPlatform.android_x86;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
_applicationPlatform = TargetPlatform.android_arm;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
late final Future<TargetPlatform> targetPlatform = () async {
|
|
|
|
|
// http://developer.android.com/ndk/guides/abis.html (x86, armeabi-v7a, ...)
|
|
|
|
|
switch (await _getProperty('ro.product.cpu.abi')) {
|
|
|
|
|
case 'arm64-v8a':
|
|
|
|
|
// Perform additional verification for 64 bit ABI. Some devices,
|
|
|
|
|
// like the Kindle Fire 8, misreport the abilist. We might not
|
|
|
|
|
// be able to retrieve this property, in which case we fall back
|
|
|
|
|
// to assuming 64 bit.
|
|
|
|
|
final String? abilist = await _getProperty('ro.product.cpu.abilist');
|
|
|
|
|
if (abilist == null || abilist.contains('arm64-v8a')) {
|
|
|
|
|
return TargetPlatform.android_arm64;
|
|
|
|
|
} else {
|
|
|
|
|
return TargetPlatform.android_arm;
|
|
|
|
|
}
|
|
|
|
|
case 'x86_64':
|
|
|
|
|
return TargetPlatform.android_x64;
|
|
|
|
|
case 'x86':
|
|
|
|
|
return TargetPlatform.android_x86;
|
|
|
|
|
default:
|
|
|
|
|
return TargetPlatform.android_arm;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return _applicationPlatform;
|
|
|
|
|
}
|
|
|
|
|
}();
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<bool> supportsRuntimeMode(BuildMode buildMode) async {
|
|
|
|
@ -249,28 +232,27 @@ class AndroidDevice extends Device {
|
|
|
|
|
case TargetPlatform.windows_x64:
|
|
|
|
|
throw UnsupportedError('Invalid target platform for Android');
|
|
|
|
|
}
|
|
|
|
|
throw null; // dead code, remove after null migration
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<String> get sdkNameAndVersion async => 'Android ${await _sdkVersion} (API ${await apiVersion})';
|
|
|
|
|
|
|
|
|
|
Future<String> get _sdkVersion => _getProperty('ro.build.version.release');
|
|
|
|
|
Future<String?> get _sdkVersion => _getProperty('ro.build.version.release');
|
|
|
|
|
|
|
|
|
|
@visibleForTesting
|
|
|
|
|
Future<String> get apiVersion => _getProperty('ro.build.version.sdk');
|
|
|
|
|
Future<String?> get apiVersion => _getProperty('ro.build.version.sdk');
|
|
|
|
|
|
|
|
|
|
AdbLogReader _logReader;
|
|
|
|
|
AdbLogReader _pastLogReader;
|
|
|
|
|
AndroidDevicePortForwarder _portForwarder;
|
|
|
|
|
AdbLogReader? _logReader;
|
|
|
|
|
AdbLogReader? _pastLogReader;
|
|
|
|
|
AndroidDevicePortForwarder? _portForwarder;
|
|
|
|
|
|
|
|
|
|
List<String> adbCommandForDevice(List<String> args) {
|
|
|
|
|
return <String>[_androidSdk.adbPath, '-s', id, ...args];
|
|
|
|
|
return <String>[_androidSdk.adbPath!, '-s', id, ...args];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<RunResult> runAdbCheckedAsync(
|
|
|
|
|
List<String> params, {
|
|
|
|
|
String workingDirectory,
|
|
|
|
|
String? workingDirectory,
|
|
|
|
|
bool allowReentrantFlutter = false,
|
|
|
|
|
}) async {
|
|
|
|
|
return _processUtils.run(
|
|
|
|
@ -284,11 +266,11 @@ class AndroidDevice extends Device {
|
|
|
|
|
|
|
|
|
|
bool _isValidAdbVersion(String adbVersion) {
|
|
|
|
|
// Sample output: 'Android Debug Bridge version 1.0.31'
|
|
|
|
|
final Match versionFields = RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
|
|
|
|
|
final Match? versionFields = RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
|
|
|
|
|
if (versionFields != null) {
|
|
|
|
|
final int majorVersion = int.parse(versionFields[1]);
|
|
|
|
|
final int minorVersion = int.parse(versionFields[2]);
|
|
|
|
|
final int patchVersion = int.parse(versionFields[3]);
|
|
|
|
|
final int majorVersion = int.parse(versionFields[1]!);
|
|
|
|
|
final int minorVersion = int.parse(versionFields[2]!);
|
|
|
|
|
final int patchVersion = int.parse(versionFields[3]!);
|
|
|
|
|
if (majorVersion > 1) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
@ -306,19 +288,20 @@ class AndroidDevice extends Device {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<bool> _checkForSupportedAdbVersion() async {
|
|
|
|
|
if (_androidSdk == null) {
|
|
|
|
|
final String? adbPath = _androidSdk.adbPath;
|
|
|
|
|
if (adbPath == null) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
final RunResult adbVersion = await _processUtils.run(
|
|
|
|
|
<String>[_androidSdk.adbPath, 'version'],
|
|
|
|
|
<String>[adbPath, 'version'],
|
|
|
|
|
throwOnError: true,
|
|
|
|
|
);
|
|
|
|
|
if (_isValidAdbVersion(adbVersion.stdout)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
_logger.printError('The ADB at "${_androidSdk.adbPath}" is too old; please install version 1.0.39 or later.');
|
|
|
|
|
_logger.printError('The ADB at "$adbPath" is too old; please install version 1.0.39 or later.');
|
|
|
|
|
} on Exception catch (error, trace) {
|
|
|
|
|
_logger.printError('Error running ADB: $error', stackTrace: trace);
|
|
|
|
|
}
|
|
|
|
@ -327,13 +310,17 @@ class AndroidDevice extends Device {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<bool> _checkForSupportedAndroidVersion() async {
|
|
|
|
|
final String? adbPath = _androidSdk.adbPath;
|
|
|
|
|
if (adbPath == null) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
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 *
|
|
|
|
|
await _processUtils.run(
|
|
|
|
|
<String>[_androidSdk.adbPath, 'start-server'],
|
|
|
|
|
<String>[adbPath, 'start-server'],
|
|
|
|
|
throwOnError: true,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
@ -343,7 +330,7 @@ class AndroidDevice extends Device {
|
|
|
|
|
final String sdkVersion = await _getProperty('ro.build.version.sdk')
|
|
|
|
|
?? minApiLevel.toString();
|
|
|
|
|
|
|
|
|
|
final int sdkVersionParsed = int.tryParse(sdkVersion);
|
|
|
|
|
final int? sdkVersionParsed = int.tryParse(sdkVersion);
|
|
|
|
|
if (sdkVersionParsed == null) {
|
|
|
|
|
_logger.printError('Unexpected response from getprop: "$sdkVersion"');
|
|
|
|
|
return false;
|
|
|
|
@ -385,7 +372,7 @@ class AndroidDevice extends Device {
|
|
|
|
|
@override
|
|
|
|
|
Future<bool> isAppInstalled(
|
|
|
|
|
AndroidApk app, {
|
|
|
|
|
String userIdentifier,
|
|
|
|
|
String? userIdentifier,
|
|
|
|
|
}) async {
|
|
|
|
|
// This call takes 400ms - 600ms.
|
|
|
|
|
try {
|
|
|
|
@ -414,9 +401,9 @@ class AndroidDevice extends Device {
|
|
|
|
|
@override
|
|
|
|
|
Future<bool> installApp(
|
|
|
|
|
AndroidApk app, {
|
|
|
|
|
String userIdentifier,
|
|
|
|
|
String? userIdentifier,
|
|
|
|
|
}) async {
|
|
|
|
|
if (!await _isAdbValid()) {
|
|
|
|
|
if (!await _adbIsValid) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
final bool wasInstalled = await isAppInstalled(app, userIdentifier: userIdentifier);
|
|
|
|
@ -446,7 +433,7 @@ class AndroidDevice extends Device {
|
|
|
|
|
|
|
|
|
|
Future<bool> _installApp(
|
|
|
|
|
AndroidApk app, {
|
|
|
|
|
String userIdentifier,
|
|
|
|
|
String? userIdentifier,
|
|
|
|
|
}) async {
|
|
|
|
|
if (!app.file.existsSync()) {
|
|
|
|
|
_logger.printError('"${_fileSystem.path.relative(app.file.path)}" does not exist.');
|
|
|
|
@ -469,7 +456,7 @@ class AndroidDevice extends Device {
|
|
|
|
|
// Some versions of adb exit with exit code 0 even on failure :(
|
|
|
|
|
// Parsing the output to check for failures.
|
|
|
|
|
final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true);
|
|
|
|
|
final String failure = failureExp.stringMatch(installResult.stdout);
|
|
|
|
|
final String? failure = failureExp.stringMatch(installResult.stdout);
|
|
|
|
|
if (failure != null) {
|
|
|
|
|
_logger.printError('Package install error: $failure');
|
|
|
|
|
return false;
|
|
|
|
@ -497,9 +484,9 @@ class AndroidDevice extends Device {
|
|
|
|
|
@override
|
|
|
|
|
Future<bool> uninstallApp(
|
|
|
|
|
AndroidApk app, {
|
|
|
|
|
String userIdentifier,
|
|
|
|
|
String? userIdentifier,
|
|
|
|
|
}) async {
|
|
|
|
|
if (!await _isAdbValid()) {
|
|
|
|
|
if (!await _adbIsValid) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -519,7 +506,7 @@ class AndroidDevice extends Device {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true);
|
|
|
|
|
final String failure = failureExp.stringMatch(uninstallOut);
|
|
|
|
|
final String? failure = failureExp.stringMatch(uninstallOut);
|
|
|
|
|
if (failure != null) {
|
|
|
|
|
_logger.printError('Package uninstall error: $failure');
|
|
|
|
|
return false;
|
|
|
|
@ -528,25 +515,24 @@ class AndroidDevice extends Device {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Whether the adb and Android versions are aligned.
|
|
|
|
|
bool _adbIsValid;
|
|
|
|
|
Future<bool> _isAdbValid() async {
|
|
|
|
|
return _adbIsValid ??= await _checkForSupportedAdbVersion() && await _checkForSupportedAndroidVersion();
|
|
|
|
|
}
|
|
|
|
|
late final Future<bool> _adbIsValid = () async {
|
|
|
|
|
return await _checkForSupportedAdbVersion() && await _checkForSupportedAndroidVersion();
|
|
|
|
|
}();
|
|
|
|
|
|
|
|
|
|
AndroidApk _package;
|
|
|
|
|
AndroidApk? _package;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<LaunchResult> startApp(
|
|
|
|
|
AndroidApk package, {
|
|
|
|
|
String mainPath,
|
|
|
|
|
String route,
|
|
|
|
|
DebuggingOptions debuggingOptions,
|
|
|
|
|
Map<String, dynamic> platformArgs = const <String, Object>{},
|
|
|
|
|
String? mainPath,
|
|
|
|
|
String? route,
|
|
|
|
|
required DebuggingOptions debuggingOptions,
|
|
|
|
|
Map<String, Object?> platformArgs = const <String, Object>{},
|
|
|
|
|
bool prebuiltApplication = false,
|
|
|
|
|
bool ipv6 = false,
|
|
|
|
|
String userIdentifier,
|
|
|
|
|
String? userIdentifier,
|
|
|
|
|
}) async {
|
|
|
|
|
if (!await _isAdbValid()) {
|
|
|
|
|
if (!await _adbIsValid) {
|
|
|
|
|
return LaunchResult.failed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -589,19 +575,19 @@ class AndroidDevice extends Device {
|
|
|
|
|
if (!prebuiltApplication || _androidSdk.licensesAvailable && _androidSdk.latestVersion == null) {
|
|
|
|
|
_logger.printTrace('Building APK');
|
|
|
|
|
final FlutterProject project = FlutterProject.current();
|
|
|
|
|
await androidBuilder.buildApk(
|
|
|
|
|
await androidBuilder!.buildApk(
|
|
|
|
|
project: project,
|
|
|
|
|
target: mainPath,
|
|
|
|
|
target: mainPath ?? 'lib/main.dart',
|
|
|
|
|
androidBuildInfo: AndroidBuildInfo(
|
|
|
|
|
debuggingOptions.buildInfo,
|
|
|
|
|
targetArchs: <AndroidArch>[androidArch],
|
|
|
|
|
fastStart: debuggingOptions.fastStart,
|
|
|
|
|
multidexEnabled: (platformArgs['multidex'] as bool) ?? false,
|
|
|
|
|
multidexEnabled: (platformArgs['multidex'] as bool?) ?? false,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
// Package has been built, so we can get the updated application ID and
|
|
|
|
|
// activity name from the .apk.
|
|
|
|
|
package = await ApplicationPackageFactory.instance
|
|
|
|
|
package = await ApplicationPackageFactory.instance!
|
|
|
|
|
.getPackageForPlatform(devicePlatform, buildInfo: debuggingOptions.buildInfo) as AndroidApk;
|
|
|
|
|
}
|
|
|
|
|
// There was a failure parsing the android project information.
|
|
|
|
@ -616,8 +602,8 @@ class AndroidDevice extends Device {
|
|
|
|
|
return LaunchResult.failed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final bool traceStartup = platformArgs['trace-startup'] as bool ?? false;
|
|
|
|
|
ProtocolDiscovery observatoryDiscovery;
|
|
|
|
|
final bool traceStartup = platformArgs['trace-startup'] as bool? ?? false;
|
|
|
|
|
ProtocolDiscovery? observatoryDiscovery;
|
|
|
|
|
|
|
|
|
|
if (debuggingOptions.debuggingEnabled) {
|
|
|
|
|
observatoryDiscovery = ProtocolDiscovery.observatory(
|
|
|
|
@ -637,6 +623,8 @@ class AndroidDevice extends Device {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final String dartVmFlags = computeDartVmFlags(debuggingOptions);
|
|
|
|
|
final String? traceAllowlist = debuggingOptions.traceAllowlist;
|
|
|
|
|
final String? traceSkiaAllowlist = debuggingOptions.traceSkiaAllowlist;
|
|
|
|
|
final List<String> cmd = <String>[
|
|
|
|
|
'shell', 'am', 'start',
|
|
|
|
|
'-a', 'android.intent.action.RUN',
|
|
|
|
@ -653,10 +641,10 @@ class AndroidDevice extends Device {
|
|
|
|
|
...<String>['--ez', 'skia-deterministic-rendering', 'true'],
|
|
|
|
|
if (debuggingOptions.traceSkia)
|
|
|
|
|
...<String>['--ez', 'trace-skia', 'true'],
|
|
|
|
|
if (debuggingOptions.traceAllowlist != null)
|
|
|
|
|
...<String>['--es', 'trace-allowlist', debuggingOptions.traceAllowlist],
|
|
|
|
|
if (debuggingOptions.traceSkiaAllowlist != null)
|
|
|
|
|
...<String>['--es', 'trace-skia-allowlist', debuggingOptions.traceSkiaAllowlist],
|
|
|
|
|
if (traceAllowlist != null)
|
|
|
|
|
...<String>['--es', 'trace-allowlist', traceAllowlist],
|
|
|
|
|
if (traceSkiaAllowlist != null)
|
|
|
|
|
...<String>['--es', 'trace-skia-allowlist', traceSkiaAllowlist],
|
|
|
|
|
if (debuggingOptions.traceSystrace)
|
|
|
|
|
...<String>['--ez', 'trace-systrace', 'true'],
|
|
|
|
|
if (debuggingOptions.endlessTraceBuffer)
|
|
|
|
@ -703,9 +691,9 @@ class AndroidDevice extends Device {
|
|
|
|
|
// device has printed "Observatory is listening on...".
|
|
|
|
|
_logger.printTrace('Waiting for observatory port to be available...');
|
|
|
|
|
try {
|
|
|
|
|
Uri observatoryUri;
|
|
|
|
|
Uri? observatoryUri;
|
|
|
|
|
if (debuggingOptions.buildInfo.isDebug || debuggingOptions.buildInfo.isProfile) {
|
|
|
|
|
observatoryUri = await observatoryDiscovery.uri;
|
|
|
|
|
observatoryUri = await observatoryDiscovery?.uri;
|
|
|
|
|
if (observatoryUri == null) {
|
|
|
|
|
_logger.printError(
|
|
|
|
|
'Error waiting for a debug connection: '
|
|
|
|
@ -719,7 +707,7 @@ class AndroidDevice extends Device {
|
|
|
|
|
_logger.printError('Error waiting for a debug connection: $error');
|
|
|
|
|
return LaunchResult.failed();
|
|
|
|
|
} finally {
|
|
|
|
|
await observatoryDiscovery.cancel();
|
|
|
|
|
await observatoryDiscovery?.cancel();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -735,7 +723,7 @@ class AndroidDevice extends Device {
|
|
|
|
|
@override
|
|
|
|
|
Future<bool> stopApp(
|
|
|
|
|
AndroidApk app, {
|
|
|
|
|
String userIdentifier,
|
|
|
|
|
String? userIdentifier,
|
|
|
|
|
}) {
|
|
|
|
|
if (app == null) {
|
|
|
|
|
return Future<bool>.value(false);
|
|
|
|
@ -754,11 +742,16 @@ class AndroidDevice extends Device {
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<MemoryInfo> queryMemoryInfo() async {
|
|
|
|
|
final AndroidApk? package = _package;
|
|
|
|
|
if (package == null) {
|
|
|
|
|
_logger.printError('Android package unknown, skipping dumpsys meminfo.');
|
|
|
|
|
return const MemoryInfo.empty();
|
|
|
|
|
}
|
|
|
|
|
final RunResult runResult = await _processUtils.run(adbCommandForDevice(<String>[
|
|
|
|
|
'shell',
|
|
|
|
|
'dumpsys',
|
|
|
|
|
'meminfo',
|
|
|
|
|
_package.id,
|
|
|
|
|
package.id,
|
|
|
|
|
'-d',
|
|
|
|
|
]));
|
|
|
|
|
|
|
|
|
@ -775,7 +768,7 @@ class AndroidDevice extends Device {
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
FutureOr<DeviceLogReader> getLogReader({
|
|
|
|
|
AndroidApk app,
|
|
|
|
|
AndroidApk? app,
|
|
|
|
|
bool includePastLogs = false,
|
|
|
|
|
}) async {
|
|
|
|
|
// The Android log reader isn't app-specific. The `app` parameter isn't used.
|
|
|
|
@ -794,19 +787,25 @@ class AndroidDevice extends Device {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
DevicePortForwarder get portForwarder => _portForwarder ??= AndroidDevicePortForwarder(
|
|
|
|
|
processManager: _processManager,
|
|
|
|
|
logger: _logger,
|
|
|
|
|
deviceId: id,
|
|
|
|
|
adbPath: _androidSdk.adbPath,
|
|
|
|
|
);
|
|
|
|
|
late final DevicePortForwarder? portForwarder = () {
|
|
|
|
|
final String? adbPath = _androidSdk.adbPath;
|
|
|
|
|
if (adbPath == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return AndroidDevicePortForwarder(
|
|
|
|
|
processManager: _processManager,
|
|
|
|
|
logger: _logger,
|
|
|
|
|
deviceId: id,
|
|
|
|
|
adbPath: adbPath,
|
|
|
|
|
);
|
|
|
|
|
}();
|
|
|
|
|
|
|
|
|
|
static final RegExp _timeRegExp = RegExp(r'^\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}', multiLine: true);
|
|
|
|
|
|
|
|
|
|
/// 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.
|
|
|
|
|
@visibleForTesting
|
|
|
|
|
Future<String> lastLogcatTimestamp() async {
|
|
|
|
|
Future<String?> lastLogcatTimestamp() async {
|
|
|
|
|
RunResult output;
|
|
|
|
|
try {
|
|
|
|
|
output = await runAdbCheckedAsync(<String>[
|
|
|
|
@ -816,7 +815,7 @@ class AndroidDevice extends Device {
|
|
|
|
|
_logger.printError('Failed to extract the most recent timestamp from the Android log: $error.');
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
final Match timeMatch = _timeRegExp.firstMatch(output.stdout);
|
|
|
|
|
final Match? timeMatch = _timeRegExp.firstMatch(output.stdout);
|
|
|
|
|
return timeMatch?.group(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -854,7 +853,7 @@ Map<String, String> parseAdbDeviceProperties(String str) {
|
|
|
|
|
final Map<String, String> properties = <String, String>{};
|
|
|
|
|
final RegExp propertyExp = RegExp(r'\[(.*?)\]: \[(.*?)\]');
|
|
|
|
|
for (final Match match in propertyExp.allMatches(str)) {
|
|
|
|
|
properties[match.group(1)] = match.group(2);
|
|
|
|
|
properties[match.group(1)!] = match.group(2)!;
|
|
|
|
|
}
|
|
|
|
|
return properties;
|
|
|
|
|
}
|
|
|
|
@ -1011,12 +1010,7 @@ class AndroidMemoryInfo extends MemoryInfo {
|
|
|
|
|
|
|
|
|
|
/// A log reader that logs from `adb logcat`.
|
|
|
|
|
class AdbLogReader extends DeviceLogReader {
|
|
|
|
|
AdbLogReader._(this._adbProcess, this.name) {
|
|
|
|
|
_linesController = StreamController<String>.broadcast(
|
|
|
|
|
onListen: _start,
|
|
|
|
|
onCancel: _stop,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
AdbLogReader._(this._adbProcess, this.name);
|
|
|
|
|
|
|
|
|
|
@visibleForTesting
|
|
|
|
|
factory AdbLogReader.test(Process adbProcess, String name) = AdbLogReader._;
|
|
|
|
@ -1029,7 +1023,7 @@ class AdbLogReader extends DeviceLogReader {
|
|
|
|
|
}) async {
|
|
|
|
|
// logcat -T is not supported on Android releases before Lollipop.
|
|
|
|
|
const int kLollipopVersionCode = 21;
|
|
|
|
|
final int apiVersion = (String v) {
|
|
|
|
|
final int? apiVersion = (String? v) {
|
|
|
|
|
// If the API version string isn't found, conservatively assume that the
|
|
|
|
|
// version is less recent than the one we're looking for.
|
|
|
|
|
return v == null ? kLollipopVersionCode - 1 : int.tryParse(v);
|
|
|
|
@ -1052,7 +1046,7 @@ class AdbLogReader extends DeviceLogReader {
|
|
|
|
|
} else if (apiVersion != null && apiVersion >= kLollipopVersionCode) {
|
|
|
|
|
// Otherwise, filter for logs appearing past the present.
|
|
|
|
|
// '-T 0` means the timestamp of the logcat command invocation.
|
|
|
|
|
final String lastLogcatTimestamp = await device.lastLogcatTimestamp();
|
|
|
|
|
final String? lastLogcatTimestamp = await device.lastLogcatTimestamp();
|
|
|
|
|
args.addAll(<String>[
|
|
|
|
|
'-T',
|
|
|
|
|
if (lastLogcatTimestamp != null) "'$lastLogcatTimestamp'" else '0',
|
|
|
|
@ -1067,7 +1061,10 @@ class AdbLogReader extends DeviceLogReader {
|
|
|
|
|
@override
|
|
|
|
|
final String name;
|
|
|
|
|
|
|
|
|
|
StreamController<String> _linesController;
|
|
|
|
|
late final StreamController<String> _linesController = StreamController<String>.broadcast(
|
|
|
|
|
onListen: _start,
|
|
|
|
|
onCancel: _stop,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Stream<String> get logLines => _linesController.stream;
|
|
|
|
@ -1128,14 +1125,14 @@ class AdbLogReader extends DeviceLogReader {
|
|
|
|
|
if (_linesController.isClosed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
final Match timeMatch = AndroidDevice._timeRegExp.firstMatch(line);
|
|
|
|
|
final Match? timeMatch = AndroidDevice._timeRegExp.firstMatch(line);
|
|
|
|
|
if (timeMatch == null || line.length == timeMatch.end) {
|
|
|
|
|
_acceptedLastLine = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Chop off the time.
|
|
|
|
|
line = line.substring(timeMatch.end + 1);
|
|
|
|
|
final Match logMatch = _logFormat.firstMatch(line);
|
|
|
|
|
final Match? logMatch = _logFormat.firstMatch(line);
|
|
|
|
|
if (logMatch != null) {
|
|
|
|
|
bool acceptLine = false;
|
|
|
|
|
|
|
|
|
@ -1143,19 +1140,19 @@ class AdbLogReader extends DeviceLogReader {
|
|
|
|
|
// While a fatal crash is going on, only accept lines from the crash
|
|
|
|
|
// Otherwise the crash log in the console may get interrupted
|
|
|
|
|
|
|
|
|
|
final Match fatalMatch = _tombstoneLine.firstMatch(line);
|
|
|
|
|
final Match? fatalMatch = _tombstoneLine.firstMatch(line);
|
|
|
|
|
|
|
|
|
|
if (fatalMatch != null) {
|
|
|
|
|
acceptLine = true;
|
|
|
|
|
|
|
|
|
|
line = fatalMatch[1];
|
|
|
|
|
line = fatalMatch[1]!;
|
|
|
|
|
|
|
|
|
|
if (_tombstoneTerminator.hasMatch(fatalMatch[1])) {
|
|
|
|
|
if (_tombstoneTerminator.hasMatch(line)) {
|
|
|
|
|
// Hit crash terminator, stop logging the crash info
|
|
|
|
|
_fatalCrash = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (appPid != null && int.parse(logMatch.group(1)) == appPid) {
|
|
|
|
|
} else if (appPid != null && int.parse(logMatch.group(1)!) == appPid) {
|
|
|
|
|
acceptLine = true;
|
|
|
|
|
|
|
|
|
|
if (_fatalLog.hasMatch(line)) {
|
|
|
|
@ -1189,7 +1186,7 @@ class AdbLogReader extends DeviceLogReader {
|
|
|
|
|
|
|
|
|
|
void _stop() {
|
|
|
|
|
_linesController.close();
|
|
|
|
|
_adbProcess?.kill();
|
|
|
|
|
_adbProcess.kill();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
@ -1201,10 +1198,10 @@ class AdbLogReader extends DeviceLogReader {
|
|
|
|
|
/// A [DevicePortForwarder] implemented for Android devices that uses adb.
|
|
|
|
|
class AndroidDevicePortForwarder extends DevicePortForwarder {
|
|
|
|
|
AndroidDevicePortForwarder({
|
|
|
|
|
@required ProcessManager processManager,
|
|
|
|
|
@required Logger logger,
|
|
|
|
|
@required String deviceId,
|
|
|
|
|
@required String adbPath,
|
|
|
|
|
required ProcessManager processManager,
|
|
|
|
|
required Logger logger,
|
|
|
|
|
required String deviceId,
|
|
|
|
|
required String adbPath,
|
|
|
|
|
}) : _deviceId = deviceId,
|
|
|
|
|
_adbPath = adbPath,
|
|
|
|
|
_logger = logger,
|
|
|
|
@ -1215,7 +1212,7 @@ class AndroidDevicePortForwarder extends DevicePortForwarder {
|
|
|
|
|
final Logger _logger;
|
|
|
|
|
final ProcessUtils _processUtils;
|
|
|
|
|
|
|
|
|
|
static int _extractPort(String portString) {
|
|
|
|
|
static int? _extractPort(String portString) {
|
|
|
|
|
return int.tryParse(portString.trim());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -1253,8 +1250,8 @@ class AndroidDevicePortForwarder extends DevicePortForwarder {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Attempt to extract ports.
|
|
|
|
|
final int hostPort = _extractPort(splitLine[1]);
|
|
|
|
|
final int devicePort = _extractPort(splitLine[2]);
|
|
|
|
|
final int? hostPort = _extractPort(splitLine[1]);
|
|
|
|
|
final int? devicePort = _extractPort(splitLine[2]);
|
|
|
|
|
|
|
|
|
|
// Failed, skip.
|
|
|
|
|
if (hostPort == null || devicePort == null) {
|
|
|
|
@ -1268,7 +1265,7 @@ class AndroidDevicePortForwarder extends DevicePortForwarder {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<int> forward(int devicePort, { int hostPort }) async {
|
|
|
|
|
Future<int> forward(int devicePort, { int? hostPort }) async {
|
|
|
|
|
hostPort ??= 0;
|
|
|
|
|
final RunResult process = await _processUtils.run(
|
|
|
|
|
<String>[
|
|
|
|
@ -1320,7 +1317,7 @@ class AndroidDevicePortForwarder extends DevicePortForwarder {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return hostPort;
|
|
|
|
|
return hostPort!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
@ -1335,7 +1332,6 @@ class AndroidDevicePortForwarder extends DevicePortForwarder {
|
|
|
|
|
'--remove',
|
|
|
|
|
tcpLine,
|
|
|
|
|
],
|
|
|
|
|
throwOnError: false,
|
|
|
|
|
);
|
|
|
|
|
if (runResult.exitCode == 0) {
|
|
|
|
|
return;
|
|
|
|
|