mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

This PR increases Android's `minSdkVersion` to 21. There are two changes in this PR aside from simply increasing the number from 19 to 21 everywhere. First, tests using `flutter_gallery` fail without updating the lockfiles. The changes in the PR are the results of running `dev/tools/bin/generate_gradle_lockfiles.dart` on that app. Second, from [here](https://developer.android.com/build/multidex#mdex-pre-l): > if your minSdkVersion is 21 or higher, multidex is enabled by default and you don't need the multidex library. As a result, the `multidex` option everywhere is obsolete. This PR removes all logic and tests related to that option that I could find. `Google testing` and `customer_tests` pass on this PR, so it seems like this won't be too breaking if it is at all. If needed I'll give this some time to bake in the framework before landing the flutter/engine PRs. Context: https://github.com/flutter/flutter/issues/138117, https://github.com/flutter/flutter/issues/141277, b/319373605
1367 lines
46 KiB
Dart
1367 lines
46 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:meta/meta.dart';
|
|
import 'package:process/process.dart';
|
|
|
|
import '../application_package.dart';
|
|
import '../base/common.dart' show throwToolExit;
|
|
import '../base/file_system.dart';
|
|
import '../base/io.dart';
|
|
import '../base/logger.dart';
|
|
import '../base/platform.dart';
|
|
import '../base/process.dart';
|
|
import '../build_info.dart';
|
|
import '../convert.dart';
|
|
import '../device.dart';
|
|
import '../device_port_forwarder.dart';
|
|
import '../project.dart';
|
|
import '../protocol_discovery.dart';
|
|
import 'android.dart';
|
|
import 'android_builder.dart';
|
|
import 'android_console.dart';
|
|
import 'android_sdk.dart';
|
|
import 'application_package.dart';
|
|
|
|
/// Whether the [AndroidDevice] is believed to be a physical device or an emulator.
|
|
enum HardwareType { emulator, physical }
|
|
|
|
/// Map to help our `isLocalEmulator` detection.
|
|
///
|
|
/// See [AndroidDevice] for more explanation of why this is needed.
|
|
const Map<String, HardwareType> kKnownHardware = <String, HardwareType>{
|
|
'goldfish': HardwareType.emulator,
|
|
'qcom': HardwareType.physical,
|
|
'ranchu': HardwareType.emulator,
|
|
'samsungexynos7420': HardwareType.physical,
|
|
'samsungexynos7580': HardwareType.physical,
|
|
'samsungexynos7870': HardwareType.physical,
|
|
'samsungexynos7880': HardwareType.physical,
|
|
'samsungexynos8890': HardwareType.physical,
|
|
'samsungexynos8895': HardwareType.physical,
|
|
'samsungexynos9810': HardwareType.physical,
|
|
'samsungexynos7570': HardwareType.physical,
|
|
};
|
|
|
|
/// A physical Android device or emulator.
|
|
///
|
|
/// While [isEmulator] attempts to distinguish between the device categories,
|
|
/// this is a best effort process and not a guarantee; certain physical devices
|
|
/// identify as emulators. These device identifiers may be added to the [kKnownHardware]
|
|
/// map to specify that they are actually physical devices.
|
|
class AndroidDevice extends Device {
|
|
AndroidDevice(
|
|
super.id, {
|
|
this.productID,
|
|
required this.modelID,
|
|
this.deviceCodeName,
|
|
required Logger logger,
|
|
required ProcessManager processManager,
|
|
required Platform platform,
|
|
required AndroidSdk androidSdk,
|
|
required FileSystem fileSystem,
|
|
AndroidConsoleSocketFactory androidConsoleSocketFactory = kAndroidConsoleSocketFactory,
|
|
}) : _logger = logger,
|
|
_processManager = processManager,
|
|
_androidSdk = androidSdk,
|
|
_platform = platform,
|
|
_fileSystem = fileSystem,
|
|
_androidConsoleSocketFactory = androidConsoleSocketFactory,
|
|
_processUtils = ProcessUtils(logger: logger, processManager: processManager),
|
|
super(
|
|
category: Category.mobile,
|
|
platformType: PlatformType.android,
|
|
ephemeral: true,
|
|
);
|
|
|
|
final Logger _logger;
|
|
final ProcessManager _processManager;
|
|
final AndroidSdk _androidSdk;
|
|
final Platform _platform;
|
|
final FileSystem _fileSystem;
|
|
final ProcessUtils _processUtils;
|
|
final AndroidConsoleSocketFactory _androidConsoleSocketFactory;
|
|
|
|
final String? productID;
|
|
final String modelID;
|
|
final String? deviceCodeName;
|
|
|
|
@override
|
|
// Wirelessly paired Android devices should have `adb-tls-connect` in the id.
|
|
// Source: https://android.googlesource.com/platform/packages/modules/adb/+/f4ba8d73079b99532069dbe888a58167b8723d6c/adb_mdns.h#30
|
|
DeviceConnectionInterface get connectionInterface =>
|
|
id.contains('adb-tls-connect')
|
|
? DeviceConnectionInterface.wireless
|
|
: DeviceConnectionInterface.attached;
|
|
|
|
late final Future<Map<String, String>> _properties = () async {
|
|
Map<String, String> properties = <String, String>{};
|
|
|
|
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');
|
|
}
|
|
return properties;
|
|
}();
|
|
|
|
Future<String?> _getProperty(String name) async {
|
|
return (await _properties)[name];
|
|
}
|
|
|
|
@override
|
|
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;
|
|
}
|
|
// 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.
|
|
///
|
|
/// The ID returned matches that in the output of `flutter emulators`. Fetching
|
|
/// this name may require connecting to the device and if an error occurs null
|
|
/// will be returned.
|
|
@override
|
|
Future<String?> get emulatorId async {
|
|
if (!(await isLocalEmulator)) {
|
|
return null;
|
|
}
|
|
|
|
// Emulators always have IDs in the format emulator-(port) where port is the
|
|
// Android Console port number.
|
|
final RegExp emulatorPortRegex = RegExp(r'emulator-(\d+)');
|
|
|
|
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)!);
|
|
_logger.printTrace('Fetching avd name for $name via Android console on $host:$port');
|
|
|
|
try {
|
|
final Socket socket = await _androidConsoleSocketFactory(host, port);
|
|
final AndroidConsole console = AndroidConsole(socket);
|
|
|
|
try {
|
|
await console
|
|
.connect()
|
|
.timeout(const Duration(seconds: 2),
|
|
onTimeout: () => throw TimeoutException('Connection timed out'));
|
|
|
|
return await console
|
|
.getAvdName()
|
|
.timeout(const Duration(seconds: 2),
|
|
onTimeout: () => throw TimeoutException('"avd name" timed out'));
|
|
} finally {
|
|
console.destroy();
|
|
}
|
|
} on Exception catch (e) {
|
|
_logger.printTrace('Failed to fetch avd name for emulator at $host:$port: $e');
|
|
// If we fail to connect to the device, we should not fail so just return
|
|
// an empty name. This data is best-effort.
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@override
|
|
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;
|
|
}
|
|
}();
|
|
|
|
@override
|
|
Future<bool> supportsRuntimeMode(BuildMode buildMode) async {
|
|
switch (await targetPlatform) {
|
|
case TargetPlatform.android_arm:
|
|
case TargetPlatform.android_arm64:
|
|
case TargetPlatform.android_x64:
|
|
return buildMode != BuildMode.jitRelease;
|
|
case TargetPlatform.android_x86:
|
|
return buildMode == BuildMode.debug;
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.darwin:
|
|
case TargetPlatform.fuchsia_arm64:
|
|
case TargetPlatform.fuchsia_x64:
|
|
case TargetPlatform.ios:
|
|
case TargetPlatform.linux_arm64:
|
|
case TargetPlatform.linux_x64:
|
|
case TargetPlatform.tester:
|
|
case TargetPlatform.web_javascript:
|
|
case TargetPlatform.windows_x64:
|
|
case TargetPlatform.windows_arm64:
|
|
throw UnsupportedError('Invalid target platform for Android');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<String> get sdkNameAndVersion async => 'Android ${await _sdkVersion} (API ${await apiVersion})';
|
|
|
|
Future<String?> get _sdkVersion => _getProperty('ro.build.version.release');
|
|
|
|
@visibleForTesting
|
|
Future<String?> get apiVersion => _getProperty('ro.build.version.sdk');
|
|
|
|
AdbLogReader? _logReader;
|
|
AdbLogReader? _pastLogReader;
|
|
|
|
List<String> adbCommandForDevice(List<String> args) {
|
|
return <String>[_androidSdk.adbPath!, '-s', id, ...args];
|
|
}
|
|
|
|
Future<RunResult> runAdbCheckedAsync(
|
|
List<String> params, {
|
|
String? workingDirectory,
|
|
bool allowReentrantFlutter = false,
|
|
}) async {
|
|
return _processUtils.run(
|
|
adbCommandForDevice(params),
|
|
throwOnError: true,
|
|
workingDirectory: workingDirectory,
|
|
allowReentrantFlutter: allowReentrantFlutter,
|
|
allowedFailures: (int value) => _allowHeapCorruptionOnWindows(value, _platform),
|
|
);
|
|
}
|
|
|
|
bool _isValidAdbVersion(String adbVersion) {
|
|
// Sample output: 'Android Debug Bridge version 1.0.31'
|
|
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]!);
|
|
if (majorVersion > 1) {
|
|
return true;
|
|
}
|
|
if (majorVersion == 1 && minorVersion > 0) {
|
|
return true;
|
|
}
|
|
if (majorVersion == 1 && minorVersion == 0 && patchVersion >= 39) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
_logger.printError(
|
|
'Unrecognized adb version string $adbVersion. Skipping version check.');
|
|
return true;
|
|
}
|
|
|
|
Future<bool> _checkForSupportedAdbVersion() async {
|
|
final String? adbPath = _androidSdk.adbPath;
|
|
if (adbPath == null) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
final RunResult adbVersion = await _processUtils.run(
|
|
<String>[adbPath, 'version'],
|
|
throwOnError: true,
|
|
);
|
|
if (_isValidAdbVersion(adbVersion.stdout)) {
|
|
return true;
|
|
}
|
|
_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);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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>[adbPath, 'start-server'],
|
|
throwOnError: true,
|
|
);
|
|
|
|
// This has been reported to return null on some devices. In this case,
|
|
// assume the lowest supported API to still allow Flutter to run.
|
|
// Sample output: '22'
|
|
final String sdkVersion = await _getProperty('ro.build.version.sdk')
|
|
?? minApiLevel.toString();
|
|
|
|
final int? sdkVersionParsed = int.tryParse(sdkVersion);
|
|
if (sdkVersionParsed == null) {
|
|
_logger.printError('Unexpected response from getprop: "$sdkVersion"');
|
|
return false;
|
|
}
|
|
|
|
if (sdkVersionParsed < minApiLevel) {
|
|
_logger.printError(
|
|
'The Android version ($sdkVersion) on the target device is too old. Please '
|
|
'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
} on Exception catch (e, stacktrace) {
|
|
_logger.printError('Unexpected failure from adb: $e');
|
|
_logger.printError('Stacktrace: $stacktrace');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
String _getDeviceSha1Path(AndroidApk apk) {
|
|
return '/data/local/tmp/sky.${apk.id}.sha1';
|
|
}
|
|
|
|
Future<String> _getDeviceApkSha1(AndroidApk apk) async {
|
|
final RunResult result = await _processUtils.run(
|
|
adbCommandForDevice(<String>['shell', 'cat', _getDeviceSha1Path(apk)]));
|
|
return result.stdout;
|
|
}
|
|
|
|
String _getSourceSha1(AndroidApk apk) {
|
|
final File shaFile = _fileSystem.file('${apk.applicationPackage.path}.sha1');
|
|
return shaFile.existsSync() ? shaFile.readAsStringSync() : '';
|
|
}
|
|
|
|
@override
|
|
String get name => modelID;
|
|
|
|
@override
|
|
bool get supportsFlavors => true;
|
|
|
|
@override
|
|
Future<bool> isAppInstalled(
|
|
ApplicationPackage app, {
|
|
String? userIdentifier,
|
|
}) async {
|
|
// This call takes 400ms - 600ms.
|
|
try {
|
|
final RunResult listOut = await runAdbCheckedAsync(<String>[
|
|
'shell',
|
|
'pm',
|
|
'list',
|
|
'packages',
|
|
if (userIdentifier != null)
|
|
...<String>['--user', userIdentifier],
|
|
app.id,
|
|
]);
|
|
return LineSplitter.split(listOut.stdout).contains('package:${app.id}');
|
|
} on Exception catch (error) {
|
|
_logger.printTrace('$error');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> isLatestBuildInstalled(covariant AndroidApk app) async {
|
|
final String installedSha1 = await _getDeviceApkSha1(app);
|
|
return installedSha1.isNotEmpty && installedSha1 == _getSourceSha1(app);
|
|
}
|
|
|
|
@override
|
|
Future<bool> installApp(
|
|
covariant AndroidApk app, {
|
|
String? userIdentifier,
|
|
}) async {
|
|
if (!await _adbIsValid) {
|
|
return false;
|
|
}
|
|
final bool wasInstalled = await isAppInstalled(app, userIdentifier: userIdentifier);
|
|
if (wasInstalled && await isLatestBuildInstalled(app)) {
|
|
_logger.printTrace('Latest build already installed.');
|
|
return true;
|
|
}
|
|
_logger.printTrace('Installing APK.');
|
|
if (await _installApp(app, userIdentifier: userIdentifier)) {
|
|
return true;
|
|
}
|
|
_logger.printTrace('Warning: Failed to install APK.');
|
|
if (!wasInstalled) {
|
|
return false;
|
|
}
|
|
_logger.printStatus('Uninstalling old version...');
|
|
if (!await uninstallApp(app, userIdentifier: userIdentifier)) {
|
|
_logger.printError('Error: Uninstalling old version failed.');
|
|
return false;
|
|
}
|
|
if (!await _installApp(app, userIdentifier: userIdentifier)) {
|
|
_logger.printError('Error: Failed to install APK again.');
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Future<bool> _installApp(
|
|
AndroidApk app, {
|
|
String? userIdentifier,
|
|
}) async {
|
|
if (!app.applicationPackage.existsSync()) {
|
|
_logger.printError('"${_fileSystem.path.relative(app.applicationPackage.path)}" does not exist.');
|
|
return false;
|
|
}
|
|
|
|
final Status status = _logger.startProgress(
|
|
'Installing ${_fileSystem.path.relative(app.applicationPackage.path)}...',
|
|
);
|
|
final RunResult installResult = await _processUtils.run(
|
|
adbCommandForDevice(<String>[
|
|
'install',
|
|
'-t',
|
|
'-r',
|
|
if (userIdentifier != null)
|
|
...<String>['--user', userIdentifier],
|
|
app.applicationPackage.path,
|
|
]));
|
|
status.stop();
|
|
// 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);
|
|
if (failure != null) {
|
|
_logger.printError('Package install error: $failure');
|
|
return false;
|
|
}
|
|
if (installResult.exitCode != 0) {
|
|
if (installResult.stderr.contains('Bad user number')) {
|
|
_logger.printError('Error: User "$userIdentifier" not found. Run "adb shell pm list users" to see list of available identifiers.');
|
|
} else {
|
|
_logger.printError('Error: ADB exited with exit code ${installResult.exitCode}');
|
|
_logger.printError('$installResult');
|
|
}
|
|
return false;
|
|
}
|
|
try {
|
|
await runAdbCheckedAsync(<String>[
|
|
'shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app),
|
|
]);
|
|
} on ProcessException catch (error) {
|
|
_logger.printError('adb shell failed to write the SHA hash: $error.');
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Future<bool> uninstallApp(
|
|
ApplicationPackage app, {
|
|
String? userIdentifier,
|
|
}) async {
|
|
if (!await _adbIsValid) {
|
|
return false;
|
|
}
|
|
|
|
String uninstallOut;
|
|
try {
|
|
final RunResult uninstallResult = await _processUtils.run(
|
|
adbCommandForDevice(<String>[
|
|
'uninstall',
|
|
if (userIdentifier != null)
|
|
...<String>['--user', userIdentifier],
|
|
app.id,
|
|
]),
|
|
throwOnError: true,
|
|
);
|
|
uninstallOut = uninstallResult.stdout;
|
|
} on Exception catch (error) {
|
|
_logger.printError('adb uninstall failed: $error');
|
|
return false;
|
|
}
|
|
final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true);
|
|
final String? failure = failureExp.stringMatch(uninstallOut);
|
|
if (failure != null) {
|
|
_logger.printError('Package uninstall error: $failure');
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Whether the adb and Android versions are aligned.
|
|
late final Future<bool> _adbIsValid = () async {
|
|
return await _checkForSupportedAdbVersion() && await _checkForSupportedAndroidVersion();
|
|
}();
|
|
|
|
AndroidApk? _package;
|
|
|
|
@override
|
|
Future<LaunchResult> startApp(
|
|
AndroidApk? package, {
|
|
String? mainPath,
|
|
String? route,
|
|
required DebuggingOptions debuggingOptions,
|
|
Map<String, Object?> platformArgs = const <String, Object>{},
|
|
bool prebuiltApplication = false,
|
|
bool ipv6 = false,
|
|
String? userIdentifier,
|
|
}) async {
|
|
if (!await _adbIsValid) {
|
|
return LaunchResult.failed();
|
|
}
|
|
|
|
final TargetPlatform devicePlatform = await targetPlatform;
|
|
if (devicePlatform == TargetPlatform.android_x86 &&
|
|
!debuggingOptions.buildInfo.isDebug) {
|
|
_logger.printError('Profile and release builds are only supported on ARM/x64 targets.');
|
|
return LaunchResult.failed();
|
|
}
|
|
|
|
AndroidApk? builtPackage = package;
|
|
AndroidArch androidArch;
|
|
switch (devicePlatform) {
|
|
case TargetPlatform.android_arm:
|
|
androidArch = AndroidArch.armeabi_v7a;
|
|
case TargetPlatform.android_arm64:
|
|
androidArch = AndroidArch.arm64_v8a;
|
|
case TargetPlatform.android_x64:
|
|
androidArch = AndroidArch.x86_64;
|
|
case TargetPlatform.android_x86:
|
|
androidArch = AndroidArch.x86;
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.darwin:
|
|
case TargetPlatform.fuchsia_arm64:
|
|
case TargetPlatform.fuchsia_x64:
|
|
case TargetPlatform.ios:
|
|
case TargetPlatform.linux_arm64:
|
|
case TargetPlatform.linux_x64:
|
|
case TargetPlatform.tester:
|
|
case TargetPlatform.web_javascript:
|
|
case TargetPlatform.windows_arm64:
|
|
case TargetPlatform.windows_x64:
|
|
_logger.printError('Android platforms are only supported.');
|
|
return LaunchResult.failed();
|
|
}
|
|
|
|
if (!prebuiltApplication || _androidSdk.licensesAvailable && _androidSdk.latestVersion == null) {
|
|
_logger.printTrace('Building APK');
|
|
final FlutterProject project = FlutterProject.current();
|
|
await androidBuilder!.buildApk(
|
|
project: project,
|
|
target: mainPath ?? 'lib/main.dart',
|
|
androidBuildInfo: AndroidBuildInfo(
|
|
debuggingOptions.buildInfo,
|
|
targetArchs: <AndroidArch>[androidArch],
|
|
fastStart: debuggingOptions.fastStart,
|
|
),
|
|
);
|
|
// Package has been built, so we can get the updated application ID and
|
|
// activity name from the .apk.
|
|
builtPackage = await ApplicationPackageFactory.instance!
|
|
.getPackageForPlatform(devicePlatform, buildInfo: debuggingOptions.buildInfo) as AndroidApk?;
|
|
}
|
|
// There was a failure parsing the android project information.
|
|
if (builtPackage == null) {
|
|
throwToolExit('Problem building Android application: see above error(s).');
|
|
}
|
|
|
|
_logger.printTrace("Stopping app '${builtPackage.name}' on $name.");
|
|
await stopApp(builtPackage, userIdentifier: userIdentifier);
|
|
|
|
if (!await installApp(builtPackage, userIdentifier: userIdentifier)) {
|
|
return LaunchResult.failed();
|
|
}
|
|
|
|
final bool traceStartup = platformArgs['trace-startup'] as bool? ?? false;
|
|
ProtocolDiscovery? vmServiceDiscovery;
|
|
|
|
if (debuggingOptions.debuggingEnabled) {
|
|
vmServiceDiscovery = ProtocolDiscovery.vmService(
|
|
// Avoid using getLogReader, which returns a singleton instance, because the
|
|
// VM Service discovery will dispose at the end. creating a new logger here allows
|
|
// logs to be surfaced normally during `flutter drive`.
|
|
await AdbLogReader.createLogReader(
|
|
this,
|
|
_processManager,
|
|
),
|
|
portForwarder: portForwarder,
|
|
hostPort: debuggingOptions.hostVmServicePort,
|
|
devicePort: debuggingOptions.deviceVmServicePort,
|
|
ipv6: ipv6,
|
|
logger: _logger,
|
|
);
|
|
}
|
|
|
|
final String dartVmFlags = computeDartVmFlags(debuggingOptions);
|
|
final String? traceAllowlist = debuggingOptions.traceAllowlist;
|
|
final String? traceSkiaAllowlist = debuggingOptions.traceSkiaAllowlist;
|
|
final String? traceToFile = debuggingOptions.traceToFile;
|
|
final List<String> cmd = <String>[
|
|
'shell', 'am', 'start',
|
|
'-a', 'android.intent.action.MAIN',
|
|
'-c', 'android.intent.category.LAUNCHER',
|
|
'-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP
|
|
if (debuggingOptions.enableDartProfiling)
|
|
...<String>['--ez', 'enable-dart-profiling', 'true'],
|
|
if (traceStartup)
|
|
...<String>['--ez', 'trace-startup', 'true'],
|
|
if (route != null)
|
|
...<String>['--es', 'route', route],
|
|
if (debuggingOptions.enableSoftwareRendering)
|
|
...<String>['--ez', 'enable-software-rendering', 'true'],
|
|
if (debuggingOptions.skiaDeterministicRendering)
|
|
...<String>['--ez', 'skia-deterministic-rendering', 'true'],
|
|
if (debuggingOptions.traceSkia)
|
|
...<String>['--ez', 'trace-skia', 'true'],
|
|
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 (traceToFile != null)
|
|
...<String>['--es', 'trace-to-file', traceToFile],
|
|
if (debuggingOptions.endlessTraceBuffer)
|
|
...<String>['--ez', 'endless-trace-buffer', 'true'],
|
|
if (debuggingOptions.dumpSkpOnShaderCompilation)
|
|
...<String>['--ez', 'dump-skp-on-shader-compilation', 'true'],
|
|
if (debuggingOptions.cacheSkSL)
|
|
...<String>['--ez', 'cache-sksl', 'true'],
|
|
if (debuggingOptions.purgePersistentCache)
|
|
...<String>['--ez', 'purge-persistent-cache', 'true'],
|
|
if (debuggingOptions.enableImpeller == ImpellerStatus.enabled)
|
|
...<String>['--ez', 'enable-impeller', 'true'],
|
|
if (debuggingOptions.enableImpeller == ImpellerStatus.disabled)
|
|
...<String>['--ez', 'enable-impeller', 'false'],
|
|
if (debuggingOptions.enableVulkanValidation)
|
|
...<String>['--ez', 'enable-vulkan-validation', 'true'],
|
|
if (debuggingOptions.debuggingEnabled) ...<String>[
|
|
if (debuggingOptions.buildInfo.isDebug) ...<String>[
|
|
...<String>['--ez', 'enable-checked-mode', 'true'],
|
|
...<String>['--ez', 'verify-entry-points', 'true'],
|
|
],
|
|
if (debuggingOptions.startPaused)
|
|
...<String>['--ez', 'start-paused', 'true'],
|
|
if (debuggingOptions.disableServiceAuthCodes)
|
|
...<String>['--ez', 'disable-service-auth-codes', 'true'],
|
|
if (dartVmFlags.isNotEmpty)
|
|
...<String>['--es', 'dart-flags', dartVmFlags],
|
|
if (debuggingOptions.useTestFonts)
|
|
...<String>['--ez', 'use-test-fonts', 'true'],
|
|
if (debuggingOptions.verboseSystemLogs)
|
|
...<String>['--ez', 'verbose-logging', 'true'],
|
|
if (userIdentifier != null)
|
|
...<String>['--user', userIdentifier],
|
|
],
|
|
builtPackage.launchActivity,
|
|
];
|
|
final String result = (await runAdbCheckedAsync(cmd)).stdout;
|
|
// This invocation returns 0 even when it fails.
|
|
if (result.contains('Error: ')) {
|
|
_logger.printError(result.trim(), wrap: false);
|
|
return LaunchResult.failed();
|
|
}
|
|
|
|
_package = builtPackage;
|
|
if (!debuggingOptions.debuggingEnabled) {
|
|
return LaunchResult.succeeded();
|
|
}
|
|
|
|
// Wait for the service protocol port here. This will complete once the
|
|
// device has printed "VM Service is listening on...".
|
|
_logger.printTrace('Waiting for VM Service port to be available...');
|
|
try {
|
|
Uri? vmServiceUri;
|
|
if (debuggingOptions.buildInfo.isDebug || debuggingOptions.buildInfo.isProfile) {
|
|
vmServiceUri = await vmServiceDiscovery?.uri;
|
|
if (vmServiceUri == null) {
|
|
_logger.printError(
|
|
'Error waiting for a debug connection: '
|
|
'The log reader stopped unexpectedly',
|
|
);
|
|
return LaunchResult.failed();
|
|
}
|
|
}
|
|
return LaunchResult.succeeded(vmServiceUri: vmServiceUri);
|
|
} on Exception catch (error) {
|
|
_logger.printError('Error waiting for a debug connection: $error');
|
|
return LaunchResult.failed();
|
|
} finally {
|
|
await vmServiceDiscovery?.cancel();
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool get supportsHotReload => true;
|
|
|
|
@override
|
|
bool get supportsHotRestart => true;
|
|
|
|
@override
|
|
bool get supportsFastStart => true;
|
|
|
|
@override
|
|
Future<bool> stopApp(
|
|
ApplicationPackage? app, {
|
|
String? userIdentifier,
|
|
}) async {
|
|
if (app == null) {
|
|
return false;
|
|
}
|
|
final List<String> command = adbCommandForDevice(<String>[
|
|
'shell',
|
|
'am',
|
|
'force-stop',
|
|
if (userIdentifier != null)
|
|
...<String>['--user', userIdentifier],
|
|
app.id,
|
|
]);
|
|
return _processUtils.stream(command).then<bool>(
|
|
(int exitCode) => exitCode == 0 || _allowHeapCorruptionOnWindows(exitCode, _platform));
|
|
}
|
|
|
|
@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,
|
|
'-d',
|
|
]));
|
|
|
|
if (runResult.exitCode != 0) {
|
|
return const MemoryInfo.empty();
|
|
}
|
|
return parseMeminfoDump(runResult.stdout);
|
|
}
|
|
|
|
@override
|
|
void clearLogs() {
|
|
_processUtils.runSync(adbCommandForDevice(<String>['logcat', '-c']));
|
|
}
|
|
|
|
@override
|
|
FutureOr<DeviceLogReader> getLogReader({
|
|
ApplicationPackage? app,
|
|
bool includePastLogs = false,
|
|
}) async {
|
|
// The Android log reader isn't app-specific. The `app` parameter isn't used.
|
|
if (includePastLogs) {
|
|
return _pastLogReader ??= await AdbLogReader.createLogReader(
|
|
this,
|
|
_processManager,
|
|
includePastLogs: true,
|
|
);
|
|
} else {
|
|
return _logReader ??= await AdbLogReader.createLogReader(
|
|
this,
|
|
_processManager,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
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 {
|
|
RunResult output;
|
|
try {
|
|
output = await runAdbCheckedAsync(<String>[
|
|
'shell', '-x', 'logcat', '-v', 'time', '-t', '1',
|
|
]);
|
|
} on Exception catch (error) {
|
|
_logger.printError('Failed to extract the most recent timestamp from the Android log: $error.');
|
|
return null;
|
|
}
|
|
final Match? timeMatch = _timeRegExp.firstMatch(output.stdout);
|
|
return timeMatch?.group(0);
|
|
}
|
|
|
|
@override
|
|
bool isSupported() => true;
|
|
|
|
@override
|
|
bool get supportsScreenshot => true;
|
|
|
|
@override
|
|
Future<void> takeScreenshot(File outputFile) async {
|
|
const String remotePath = '/data/local/tmp/flutter_screenshot.png';
|
|
await runAdbCheckedAsync(<String>['shell', 'screencap', '-p', remotePath]);
|
|
await _processUtils.run(
|
|
adbCommandForDevice(<String>['pull', remotePath, outputFile.path]),
|
|
throwOnError: true,
|
|
);
|
|
await runAdbCheckedAsync(<String>['shell', 'rm', remotePath]);
|
|
}
|
|
|
|
@override
|
|
bool isSupportedForProject(FlutterProject flutterProject) {
|
|
return flutterProject.android.existsSync();
|
|
}
|
|
|
|
@override
|
|
Future<void> dispose() async {
|
|
_logReader?._stop();
|
|
_pastLogReader?._stop();
|
|
}
|
|
}
|
|
|
|
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)!;
|
|
}
|
|
return properties;
|
|
}
|
|
|
|
/// Process the dumpsys info formatted in a table-like structure.
|
|
///
|
|
/// Currently this only pulls information from the "App Summary" subsection.
|
|
///
|
|
/// Example output:
|
|
///
|
|
/// ```
|
|
/// Applications Memory Usage (in Kilobytes):
|
|
/// Uptime: 441088659 Realtime: 521464097
|
|
///
|
|
/// ** MEMINFO in pid 16141 [io.flutter.demo.gallery] **
|
|
/// Pss Private Private SwapPss Heap Heap Heap
|
|
/// Total Dirty Clean Dirty Size Alloc Free
|
|
/// ------ ------ ------ ------ ------ ------ ------
|
|
/// Native Heap 8648 8620 0 16 20480 12403 8076
|
|
/// Dalvik Heap 547 424 40 18 2628 1092 1536
|
|
/// Dalvik Other 464 464 0 0
|
|
/// Stack 496 496 0 0
|
|
/// Ashmem 2 0 0 0
|
|
/// Gfx dev 212 204 0 0
|
|
/// Other dev 48 0 48 0
|
|
/// .so mmap 10770 708 9372 25
|
|
/// .apk mmap 240 0 0 0
|
|
/// .ttf mmap 35 0 32 0
|
|
/// .dex mmap 2205 4 1172 0
|
|
/// .oat mmap 64 0 0 0
|
|
/// .art mmap 4228 3848 24 2
|
|
/// Other mmap 20713 4 20704 0
|
|
/// GL mtrack 2380 2380 0 0
|
|
/// Unknown 43971 43968 0 1
|
|
/// TOTAL 95085 61120 31392 62 23108 13495 9612
|
|
///
|
|
/// App Summary
|
|
/// Pss(KB)
|
|
/// ------
|
|
/// Java Heap: 4296
|
|
/// Native Heap: 8620
|
|
/// Code: 11288
|
|
/// Stack: 496
|
|
/// Graphics: 2584
|
|
/// Private Other: 65228
|
|
/// System: 2573
|
|
///
|
|
/// TOTAL: 95085 TOTAL SWAP PSS: 62
|
|
///
|
|
/// Objects
|
|
/// Views: 9 ViewRootImpl: 1
|
|
/// AppContexts: 3 Activities: 1
|
|
/// Assets: 4 AssetManagers: 3
|
|
/// Local Binders: 10 Proxy Binders: 18
|
|
/// Parcel memory: 6 Parcel count: 24
|
|
/// Death Recipients: 0 OpenSSL Sockets: 0
|
|
/// WebViews: 0
|
|
///
|
|
/// SQL
|
|
/// MEMORY_USED: 0
|
|
/// PAGECACHE_OVERFLOW: 0 MALLOC_SIZE: 0
|
|
/// ...
|
|
/// ```
|
|
///
|
|
/// For more information, see https://developer.android.com/studio/command-line/dumpsys.
|
|
@visibleForTesting
|
|
AndroidMemoryInfo parseMeminfoDump(String input) {
|
|
final AndroidMemoryInfo androidMemoryInfo = AndroidMemoryInfo();
|
|
|
|
final List<String> lines = input.split('\n');
|
|
|
|
final String timelineData = lines.firstWhere((String line) =>
|
|
line.startsWith('${AndroidMemoryInfo._kUpTimeKey}: '));
|
|
final List<String> times = timelineData.trim().split('${AndroidMemoryInfo._kRealTimeKey}:');
|
|
androidMemoryInfo.realTime = int.tryParse(times.last.trim()) ?? 0;
|
|
|
|
lines
|
|
.skipWhile((String line) => !line.contains('App Summary'))
|
|
.takeWhile((String line) => !line.contains('TOTAL'))
|
|
.where((String line) => line.contains(':'))
|
|
.forEach((String line) {
|
|
final List<String> sections = line.trim().split(':');
|
|
final String key = sections.first.trim();
|
|
final int value = int.tryParse(sections.last.trim()) ?? 0;
|
|
switch (key) {
|
|
case AndroidMemoryInfo._kJavaHeapKey:
|
|
androidMemoryInfo.javaHeap = value;
|
|
case AndroidMemoryInfo._kNativeHeapKey:
|
|
androidMemoryInfo.nativeHeap = value;
|
|
case AndroidMemoryInfo._kCodeKey:
|
|
androidMemoryInfo.code = value;
|
|
case AndroidMemoryInfo._kStackKey:
|
|
androidMemoryInfo.stack = value;
|
|
case AndroidMemoryInfo._kGraphicsKey:
|
|
androidMemoryInfo.graphics = value;
|
|
case AndroidMemoryInfo._kPrivateOtherKey:
|
|
androidMemoryInfo.privateOther = value;
|
|
case AndroidMemoryInfo._kSystemKey:
|
|
androidMemoryInfo.system = value;
|
|
}
|
|
});
|
|
return androidMemoryInfo;
|
|
}
|
|
|
|
/// Android specific implementation of memory info.
|
|
class AndroidMemoryInfo extends MemoryInfo {
|
|
static const String _kUpTimeKey = 'Uptime';
|
|
static const String _kRealTimeKey = 'Realtime';
|
|
static const String _kJavaHeapKey = 'Java Heap';
|
|
static const String _kNativeHeapKey = 'Native Heap';
|
|
static const String _kCodeKey = 'Code';
|
|
static const String _kStackKey = 'Stack';
|
|
static const String _kGraphicsKey = 'Graphics';
|
|
static const String _kPrivateOtherKey = 'Private Other';
|
|
static const String _kSystemKey = 'System';
|
|
static const String _kTotalKey = 'Total';
|
|
|
|
// Realtime is time since the system was booted includes deep sleep. Clock
|
|
// is monotonic, and ticks even when the CPU is in power saving modes.
|
|
int realTime = 0;
|
|
|
|
// Each measurement has KB as a unit.
|
|
int javaHeap = 0;
|
|
int nativeHeap = 0;
|
|
int code = 0;
|
|
int stack = 0;
|
|
int graphics = 0;
|
|
int privateOther = 0;
|
|
int system = 0;
|
|
|
|
@override
|
|
Map<String, Object> toJson() {
|
|
return <String, Object>{
|
|
'platform': 'Android',
|
|
_kRealTimeKey: realTime,
|
|
_kJavaHeapKey: javaHeap,
|
|
_kNativeHeapKey: nativeHeap,
|
|
_kCodeKey: code,
|
|
_kStackKey: stack,
|
|
_kGraphicsKey: graphics,
|
|
_kPrivateOtherKey: privateOther,
|
|
_kSystemKey: system,
|
|
_kTotalKey: javaHeap + nativeHeap + code + stack + graphics + privateOther + system,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// A log reader that logs from `adb logcat`.
|
|
class AdbLogReader extends DeviceLogReader {
|
|
AdbLogReader._(this._adbProcess, this.name);
|
|
|
|
@visibleForTesting
|
|
factory AdbLogReader.test(Process adbProcess, String name) = AdbLogReader._;
|
|
|
|
/// Create a new [AdbLogReader] from an [AndroidDevice] instance.
|
|
static Future<AdbLogReader> createLogReader(
|
|
AndroidDevice device,
|
|
ProcessManager processManager, {
|
|
bool includePastLogs = false,
|
|
}) async {
|
|
// logcat -T is not supported on Android releases before Lollipop.
|
|
const int kLollipopVersionCode = 21;
|
|
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);
|
|
}(await device.apiVersion);
|
|
|
|
// Start the adb logcat process and filter the most recent logs since `lastTimestamp`.
|
|
// Some devices (notably LG) will only output logcat via shell
|
|
// https://github.com/flutter/flutter/issues/51853
|
|
final List<String> args = <String>[
|
|
'shell',
|
|
'-x',
|
|
'logcat',
|
|
'-v',
|
|
'time',
|
|
];
|
|
|
|
// If past logs are included then filter for 'flutter' logs only.
|
|
if (includePastLogs) {
|
|
args.addAll(<String>['-s', 'flutter']);
|
|
} 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();
|
|
args.addAll(<String>[
|
|
'-T',
|
|
if (lastLogcatTimestamp != null) "'$lastLogcatTimestamp'" else '0',
|
|
]);
|
|
}
|
|
final Process process = await processManager.start(device.adbCommandForDevice(args));
|
|
return AdbLogReader._(process, device.name);
|
|
}
|
|
|
|
final Process _adbProcess;
|
|
|
|
@override
|
|
final String name;
|
|
|
|
late final StreamController<String> _linesController = StreamController<String>.broadcast(
|
|
onListen: _start,
|
|
onCancel: _stop,
|
|
);
|
|
|
|
@override
|
|
Stream<String> get logLines => _linesController.stream;
|
|
|
|
void _start() {
|
|
// We expect logcat streams to occasionally contain invalid utf-8,
|
|
// see: https://github.com/flutter/flutter/pull/8864.
|
|
const Utf8Decoder decoder = Utf8Decoder(reportErrors: false);
|
|
_adbProcess.stdout.transform<String>(decoder)
|
|
.transform<String>(const LineSplitter())
|
|
.listen(_onLine);
|
|
_adbProcess.stderr.transform<String>(decoder)
|
|
.transform<String>(const LineSplitter())
|
|
.listen(_onLine);
|
|
unawaited(_adbProcess.exitCode.whenComplete(() {
|
|
if (_linesController.hasListener) {
|
|
_linesController.close();
|
|
}
|
|
}));
|
|
}
|
|
|
|
// 'W/ActivityManager(pid): '
|
|
static final RegExp _logFormat = RegExp(r'^[VDIWEF]\/.*?\(\s*(\d+)\):\s');
|
|
|
|
static final List<RegExp> _allowedTags = <RegExp>[
|
|
RegExp(r'^[VDIWEF]\/flutter[^:]*:\s+', caseSensitive: false),
|
|
RegExp(r'^[IE]\/DartVM[^:]*:\s+'),
|
|
RegExp(r'^[WEF]\/AndroidRuntime:\s+'),
|
|
RegExp(r'^[WEF]\/AndroidRuntime\([0-9]+\):\s+'),
|
|
RegExp(r'^[WEF]\/ActivityManager:\s+.*(\bflutter\b|\bdomokit\b|\bsky\b)'),
|
|
RegExp(r'^[WEF]\/System\.err:\s+'),
|
|
RegExp(r'^[F]\/[\S^:]+:\s+'),
|
|
];
|
|
|
|
// E/SurfaceSyncer(22636): Failed to find sync for id=9
|
|
// Some versions of Android spew this out. It is inactionable to the end user
|
|
// and causes no problems for the application.
|
|
static final RegExp _surfaceSyncerSpam = RegExp(r'^E/SurfaceSyncer\(\s*\d+\): Failed to find sync for id=\d+');
|
|
|
|
// 'F/libc(pid): Fatal signal 11'
|
|
static final RegExp _fatalLog = RegExp(r'^F\/libc\s*\(\s*\d+\):\sFatal signal (\d+)');
|
|
|
|
// 'I/DEBUG(pid): ...'
|
|
static final RegExp _tombstoneLine = RegExp(r'^[IF]\/DEBUG\s*\(\s*\d+\):\s(.+)$');
|
|
|
|
// 'I/DEBUG(pid): Tombstone written to: '
|
|
static final RegExp _tombstoneTerminator = RegExp(r'^Tombstone written to:\s');
|
|
|
|
// we default to true in case none of the log lines match
|
|
bool _acceptedLastLine = true;
|
|
|
|
// Whether a fatal crash is happening or not.
|
|
// During a fatal crash only lines from the crash are accepted, the rest are
|
|
// dropped.
|
|
bool _fatalCrash = false;
|
|
|
|
// The format of the line is controlled by the '-v' parameter passed to
|
|
// adb logcat. We are currently passing 'time', which has the format:
|
|
// mm-dd hh:mm:ss.milliseconds Priority/Tag( PID): ....
|
|
void _onLine(String line) {
|
|
// This line might be processed after the subscription is closed but before
|
|
// adb stops streaming logs.
|
|
if (_linesController.isClosed) {
|
|
return;
|
|
}
|
|
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);
|
|
if (logMatch != null) {
|
|
bool acceptLine = false;
|
|
|
|
if (_fatalCrash) {
|
|
// 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);
|
|
|
|
if (fatalMatch != null) {
|
|
acceptLine = true;
|
|
|
|
line = 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) {
|
|
acceptLine = !_surfaceSyncerSpam.hasMatch(line);
|
|
|
|
if (_fatalLog.hasMatch(line)) {
|
|
// Hit fatal signal, app is now crashing
|
|
_fatalCrash = true;
|
|
}
|
|
} else {
|
|
// Filter on approved names and levels.
|
|
acceptLine = _allowedTags.any((RegExp re) => re.hasMatch(line));
|
|
}
|
|
|
|
if (acceptLine) {
|
|
_acceptedLastLine = true;
|
|
_linesController.add(line);
|
|
return;
|
|
}
|
|
_acceptedLastLine = false;
|
|
} else if (line == '--------- beginning of system' ||
|
|
line == '--------- beginning of main') {
|
|
// hide the ugly adb logcat log boundaries at the start
|
|
_acceptedLastLine = false;
|
|
} else {
|
|
// If it doesn't match the log pattern at all, then pass it through if we
|
|
// passed the last matching line through. It might be a multiline message.
|
|
if (_acceptedLastLine) {
|
|
_linesController.add(line);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void _stop() {
|
|
_linesController.close();
|
|
_adbProcess.kill();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_stop();
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
}) : _deviceId = deviceId,
|
|
_adbPath = adbPath,
|
|
_logger = logger,
|
|
_processUtils = ProcessUtils(logger: logger, processManager: processManager);
|
|
|
|
final String _deviceId;
|
|
final String _adbPath;
|
|
final Logger _logger;
|
|
final ProcessUtils _processUtils;
|
|
|
|
static int? _extractPort(String portString) {
|
|
return int.tryParse(portString.trim());
|
|
}
|
|
|
|
@override
|
|
List<ForwardedPort> get forwardedPorts {
|
|
final List<ForwardedPort> ports = <ForwardedPort>[];
|
|
|
|
String stdout;
|
|
try {
|
|
stdout = _processUtils.runSync(
|
|
<String>[
|
|
_adbPath,
|
|
'-s',
|
|
_deviceId,
|
|
'forward',
|
|
'--list',
|
|
],
|
|
throwOnError: true,
|
|
).stdout.trim();
|
|
} on ProcessException catch (error) {
|
|
_logger.printError('Failed to list forwarded ports: $error.');
|
|
return ports;
|
|
}
|
|
|
|
final List<String> lines = LineSplitter.split(stdout).toList();
|
|
for (final String line in lines) {
|
|
if (!line.startsWith(_deviceId)) {
|
|
continue;
|
|
}
|
|
final List<String> splitLine = line.split('tcp:');
|
|
|
|
// Sanity check splitLine.
|
|
if (splitLine.length != 3) {
|
|
continue;
|
|
}
|
|
|
|
// Attempt to extract ports.
|
|
final int? hostPort = _extractPort(splitLine[1]);
|
|
final int? devicePort = _extractPort(splitLine[2]);
|
|
|
|
// Failed, skip.
|
|
if (hostPort == null || devicePort == null) {
|
|
continue;
|
|
}
|
|
|
|
ports.add(ForwardedPort(hostPort, devicePort));
|
|
}
|
|
|
|
return ports;
|
|
}
|
|
|
|
@override
|
|
Future<int> forward(int devicePort, { int? hostPort }) async {
|
|
hostPort ??= 0;
|
|
final RunResult process = await _processUtils.run(
|
|
<String>[
|
|
_adbPath,
|
|
'-s',
|
|
_deviceId,
|
|
'forward',
|
|
'tcp:$hostPort',
|
|
'tcp:$devicePort',
|
|
],
|
|
throwOnError: true,
|
|
);
|
|
|
|
if (process.stderr.isNotEmpty) {
|
|
process.throwException('adb returned error:\n${process.stderr}');
|
|
}
|
|
|
|
if (process.exitCode != 0) {
|
|
if (process.stdout.isNotEmpty) {
|
|
process.throwException('adb returned error:\n${process.stdout}');
|
|
}
|
|
process.throwException('adb failed without a message');
|
|
}
|
|
|
|
if (hostPort == 0) {
|
|
if (process.stdout.isEmpty) {
|
|
process.throwException('adb did not report forwarded port');
|
|
}
|
|
hostPort = int.tryParse(process.stdout);
|
|
if (hostPort == null) {
|
|
process.throwException('adb returned invalid port number:\n${process.stdout}');
|
|
}
|
|
} else {
|
|
// stdout may be empty or the port we asked it to forward, though it's
|
|
// not documented (or obvious) what triggers each case.
|
|
//
|
|
// Observations are:
|
|
// - On MacOS it's always empty when Flutter spawns the process, but
|
|
// - On MacOS it prints the port number when run from the terminal, unless
|
|
// the port is already forwarded, when it also prints nothing.
|
|
// - On ChromeOS, the port appears to be printed even when Flutter spawns
|
|
// the process
|
|
//
|
|
// To cover all cases, we accept the output being either empty or exactly
|
|
// the port number, but treat any other output as probably being an error
|
|
// message.
|
|
if (process.stdout.isNotEmpty && process.stdout.trim() != '$hostPort') {
|
|
process.throwException('adb returned error:\n${process.stdout}');
|
|
}
|
|
}
|
|
|
|
return hostPort!;
|
|
}
|
|
|
|
@override
|
|
Future<void> unforward(ForwardedPort forwardedPort) async {
|
|
final String tcpLine = 'tcp:${forwardedPort.hostPort}';
|
|
final RunResult runResult = await _processUtils.run(
|
|
<String>[
|
|
_adbPath,
|
|
'-s',
|
|
_deviceId,
|
|
'forward',
|
|
'--remove',
|
|
tcpLine,
|
|
],
|
|
);
|
|
if (runResult.exitCode == 0) {
|
|
return;
|
|
}
|
|
_logger.printError('Failed to unforward port: $runResult');
|
|
}
|
|
|
|
@override
|
|
Future<void> dispose() async {
|
|
for (final ForwardedPort port in forwardedPorts) {
|
|
await unforward(port);
|
|
}
|
|
}
|
|
}
|
|
|
|
// In platform tools 29.0.0 adb.exe seems to be ending with this heap
|
|
// corruption error code on seemingly successful termination. Ignore
|
|
// this error on windows.
|
|
bool _allowHeapCorruptionOnWindows(int exitCode, Platform platform) {
|
|
return exitCode == -1073740940 && platform.isWindows;
|
|
}
|