blob: 8d5062b51cf1ab189e3e38a068d0818697ded7e3 [file] [log] [blame]
// Copyright 2020 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 'dart:convert';
import 'dart:io';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'package:retry/retry.dart';
import 'device.dart';
import 'health.dart';
import 'host_utils.dart';
import 'mac.dart';
import 'utils.dart';
// The minimum battery level to run a task with a scale of 100%.
const int _kBatteryMinLevel = 15;
// The maximum battery temprature to run a task with a Celsius degree.
const int _kBatteryMaxTemperatureInCelsius = 34;
class AndroidDeviceDiscovery implements DeviceDiscovery {
factory AndroidDeviceDiscovery(File? output) {
return _instance ??= AndroidDeviceDiscovery._(output);
}
final File? _outputFilePath;
AndroidDeviceDiscovery._(this._outputFilePath);
@visibleForTesting
AndroidDeviceDiscovery.testing(this._outputFilePath);
// Parses information about a device. Example:
//
// 015d172c98400a03 device usb:340787200X product:nakasi model:Nexus_7 device:grouper
static final RegExp _kDeviceRegex = RegExp(r'^(\S+)\s+(\S+)(.*)');
static AndroidDeviceDiscovery? _instance;
Future<String> _deviceListOutput(Duration timeout, {ProcessManager? processManager}) async {
return eval('adb', <String>['devices', '-l'], canFail: false, processManager: processManager).timeout(timeout);
}
Future<List<String>> _deviceListOutputWithRetries(Duration retryDuration, {ProcessManager? processManager}) async {
const Duration deviceOutputTimeout = Duration(seconds: 15);
final RetryOptions r = RetryOptions(
maxAttempts: 3,
delayFactor: retryDuration,
);
return r.retry(
() async {
final String result = await _deviceListOutput(deviceOutputTimeout, processManager: processManager);
return result.trim().split('\n');
},
retryIf: (Exception e) => e is TimeoutException,
onRetry: (Exception e) => _killAdbServer(processManager: processManager),
);
}
void _killAdbServer({ProcessManager? processManager}) async {
if (Platform.isWindows) {
await killAllRunningProcessesOnWindows('adb', processManager: processManager);
} else {
await eval('adb', <String>['kill-server'], canFail: false, processManager: processManager);
}
}
@override
Future<List<AndroidDevice>> discoverDevices({
Duration retryDuration = const Duration(seconds: 10),
ProcessManager? processManager,
}) async {
processManager ??= LocalProcessManager();
final List<String> output = await _deviceListOutputWithRetries(retryDuration, processManager: processManager);
final List<String> results = <String>[];
for (String line in output) {
// Skip lines like: * daemon started successfully *
if (line.startsWith('* daemon ')) continue;
if (line.startsWith('List of devices')) continue;
if (_kDeviceRegex.hasMatch(line)) {
final Match? match = _kDeviceRegex.firstMatch(line);
final String? deviceID = match?[1];
final String? deviceState = match?[2];
if (!const ['unauthorized', 'offline'].contains(deviceState)) {
results.add(deviceID!);
}
} else {
throw 'Failed to parse device from adb output: $line';
}
}
return results.map((String id) => AndroidDevice(deviceId: id)).toList();
}
@override
Future<Map<String, List<HealthCheckResult>>> checkDevices({ProcessManager? processManager}) async {
processManager ??= LocalProcessManager();
final List<HealthCheckResult> defaultChecks = <HealthCheckResult>[];
defaultChecks.add(await killAdbServerCheck(processManager: processManager));
final Map<String, List<HealthCheckResult>> results = <String, List<HealthCheckResult>>{};
for (AndroidDevice device in await discoverDevices(processManager: processManager)) {
final List<HealthCheckResult> checks = defaultChecks;
checks.add(HealthCheckResult.success(kDeviceAccessCheckKey));
checks.add(await adbPowerServiceCheck(processManager: processManager));
checks.add(await developerModeCheck(processManager: processManager));
checks.add(await screenOnCheck(processManager: processManager));
checks.add(await screenSaverCheck(processManager: processManager));
checks.add(await screenRotationCheck(processManager: processManager));
checks.add(await batteryLevelCheck(processManager: processManager));
checks.add(await batteryTemperatureCheck(processManager: processManager));
if (Platform.isMacOS) {
checks.add(await userAutoLoginCheck(processManager: processManager));
}
results['android-device-${device.deviceId}'] = checks;
}
final Map<String, Map<String, dynamic>> healthCheckMap = await healthcheck(results);
writeToFile(json.encode(healthCheckMap), _outputFilePath!);
return results;
}
/// Checks and returns the device properties, like manufacturer, base_buildid, etc.
///
/// It supports multiple devices, but here we are assuming only one device is attached.
@override
Future<Map<String, String>> deviceProperties({ProcessManager? processManager}) async {
final List<AndroidDevice> devices = await discoverDevices(processManager: processManager);
Map<String, String> properties = <String, String>{};
if (devices.isEmpty) {
writeToFile(json.encode(properties), _outputFilePath!);
stdout.writeln('No devices available.');
return properties;
}
properties = await getDeviceProperties(devices[0], processManager: processManager);
final String propertiesJson = json.encode(properties);
writeToFile(propertiesJson, _outputFilePath!);
stdout.writeln('Properties for deviceID ${devices[0].deviceId}: $propertiesJson');
return properties;
}
/// Gets android device properties based on swarming bot configuration.
///
/// Refer function `get_dimensions` from
/// https://source.chromium.org/chromium/infra/infra/+/master:luci/appengine/swarming/swarming_bot/api/platforms/android.py
Future<Map<String, String>> getDeviceProperties(AndroidDevice device, {ProcessManager? processManager}) async {
processManager ??= LocalProcessManager();
final Map<String, String> deviceProperties = <String, String>{};
final Map<String, String> propertyMap = <String, String>{};
LineSplitter.split(
await eval('adb', <String>['-s', device.deviceId!, 'shell', 'getprop'], processManager: processManager),
).forEach((String property) {
final List<String> propertyList = property.replaceAll('[', '').replaceAll(']', '').split(': ');
/// Deal with entries spanning only one line.
///
/// This is to skip unused entries spanninning multiple lines.
/// For example:
/// [persist.sys.boot.reason.history]: [reboot,ota,1613677289
/// reboot,userrequested,1613677269
/// reboot,userrequested,1613508544]
if (propertyList.length == 2) {
propertyMap[propertyList[0].trim()] = propertyList[1].trim();
}
});
deviceProperties['product_brand'] = propertyMap['ro.product.brand']!;
deviceProperties['build_id'] = propertyMap['ro.build.id']!;
deviceProperties['build_type'] = propertyMap['ro.build.type']!;
deviceProperties['product_model'] = propertyMap['ro.product.model']!;
deviceProperties['product_board'] = propertyMap['ro.product.board']!;
return deviceProperties;
}
@override
Future<void> recoverDevices() async {
for (Device device in await discoverDevices()) {
await device.recover();
}
}
@visibleForTesting
Future<HealthCheckResult> adbPowerServiceCheck({ProcessManager? processManager}) async {
HealthCheckResult healthCheckResult;
try {
await eval('adb', <String>['shell', 'dumpsys', 'power'], processManager: processManager);
healthCheckResult = HealthCheckResult.success(kAdbPowerServiceCheckKey);
} on BuildFailedError catch (error) {
healthCheckResult = HealthCheckResult.failure(kAdbPowerServiceCheckKey, error.toString());
}
return healthCheckResult;
}
@visibleForTesting
/// The health check for Android device screen on.
///
/// An Android device screen is on when both `mHoldingWakeLockSuspendBlocker` and
/// `mHoldingDisplaySuspendBlocker` are true.
Future<HealthCheckResult> screenOnCheck({ProcessManager? processManager}) async {
HealthCheckResult healthCheckResult;
try {
final String result = await eval(
'adb',
<String>['shell', 'dumpsys', 'power', '|', 'grep', 'mHoldingDisplaySuspendBlocker'],
processManager: processManager,
);
if (result.trim() == 'mHoldingDisplaySuspendBlocker=true') {
healthCheckResult = HealthCheckResult.success(kScreenOnCheckKey);
} else {
healthCheckResult = HealthCheckResult.failure(kScreenOnCheckKey, 'screen is off');
}
} on BuildFailedError catch (error) {
healthCheckResult = HealthCheckResult.failure(kScreenOnCheckKey, error.toString());
}
return healthCheckResult;
}
@visibleForTesting
/// The health check for Android device adb kill server.
///
/// Kill adb server before running any health check to avoid device quarantine:
/// https://github.com/flutter/flutter/issues/93075.
Future<HealthCheckResult> killAdbServerCheck({ProcessManager? processManager}) async {
HealthCheckResult healthCheckResult;
try {
await eval('adb', <String>['kill-server'], processManager: processManager);
healthCheckResult = HealthCheckResult.success(kKillAdbServerCheckKey);
} on BuildFailedError catch (error) {
healthCheckResult = HealthCheckResult.failure(kKillAdbServerCheckKey, error.toString());
}
return healthCheckResult;
}
@visibleForTesting
/// The health check for Android device developer mode.
///
/// Developer mode `on` is expected for a healthy Android device.
Future<HealthCheckResult> developerModeCheck({ProcessManager? processManager}) async {
HealthCheckResult healthCheckResult;
try {
final String result = await eval(
'adb',
<String>['shell', 'settings', 'get', 'global', 'development_settings_enabled'],
processManager: processManager,
);
// The output of `development_settings_enabled` is `1` when developer mode is on.
if (result == '1') {
healthCheckResult = HealthCheckResult.success(kDeveloperModeCheckKey);
} else {
healthCheckResult = HealthCheckResult.failure(kDeveloperModeCheckKey, 'developer mode is off');
}
} on BuildFailedError catch (error) {
healthCheckResult = HealthCheckResult.failure(kDeveloperModeCheckKey, error.toString());
}
return healthCheckResult;
}
/// The health check to validate screen rotation is off.
///
/// Screen rotation is expected disabled for a healthy Android device.
Future<HealthCheckResult> screenRotationCheck({ProcessManager? processManager}) async {
HealthCheckResult healthCheckResult;
try {
final String result = await eval(
'adb',
<String>['shell', 'settings', 'get', 'system', 'accelerometer_rotation'],
processManager: processManager,
);
// The output of `screensaver_enabled` is `0` when screensaver mode is off.
if (result == '0') {
healthCheckResult = HealthCheckResult.success(kScreenRotationCheckKey);
} else {
healthCheckResult = HealthCheckResult.failure(kScreenRotationCheckKey, 'Screen rotation is enabled');
}
} on BuildFailedError catch (error) {
healthCheckResult = HealthCheckResult.failure(kScreenRotationCheckKey, error.toString());
}
return healthCheckResult;
}
/// The health check to validate screensaver is off.
///
/// Screensaver`off` is expected for a healthy Android device.
Future<HealthCheckResult> screenSaverCheck({ProcessManager? processManager}) async {
HealthCheckResult healthCheckResult;
try {
final String result = await eval(
'adb',
<String>['shell', 'settings', 'get', 'secure', 'screensaver_enabled'],
processManager: processManager,
);
// The output of `screensaver_enabled` is `0` when screensaver mode is off.
if (result == '0') {
healthCheckResult = HealthCheckResult.success(kScreenSaverCheckKey);
} else {
healthCheckResult = HealthCheckResult.failure(kScreenSaverCheckKey, 'Screensaver is on');
}
} on BuildFailedError catch (error) {
healthCheckResult = HealthCheckResult.failure(kScreenSaverCheckKey, error.toString());
}
return healthCheckResult;
}
/// The health check for battery level.
Future<HealthCheckResult> batteryLevelCheck({ProcessManager? processManager}) async {
HealthCheckResult healthCheckResult;
try {
// The battery level returns two rows. For example:
// level: 100
// mod level: -1
final String levelResults = await eval(
'adb',
<String>['shell', 'dumpsys', 'battery', '|', 'grep', 'level'],
processManager: processManager,
);
final RegExp levelRegExp = RegExp('level: (?<level>.+)');
final RegExpMatch? match = levelRegExp.firstMatch(levelResults);
final int level = int.parse(match!.namedGroup('level')!);
if (level < _kBatteryMinLevel) {
healthCheckResult =
HealthCheckResult.failure(kBatteryLevelCheckKey, 'Battery level ($level) is below $_kBatteryMinLevel');
} else {
healthCheckResult = HealthCheckResult.success(kBatteryLevelCheckKey);
}
} on BuildFailedError catch (error) {
healthCheckResult = HealthCheckResult.failure(kScreenSaverCheckKey, error.toString());
}
return healthCheckResult;
}
/// The health check for battery temperature.
Future<HealthCheckResult> batteryTemperatureCheck({ProcessManager? processManager}) async {
HealthCheckResult healthCheckResult;
try {
// The battery temperature returns one row. For example:
// temperature: 240
// It means 24°C.
final String tempResult = await eval(
'adb',
<String>['shell', 'dumpsys', 'battery', '|', 'grep', 'temperature'],
processManager: processManager,
);
final RegExp? tempRegExp = RegExp('temperature: (?<temperature>.+)');
final RegExpMatch match = tempRegExp!.firstMatch(tempResult)!;
final int temperature = int.parse(match.namedGroup('temperature')!);
if (temperature > _kBatteryMaxTemperatureInCelsius * 10) {
healthCheckResult = HealthCheckResult.failure(
kBatteryTemperatureCheckKey,
'Battery temperature (${(temperature * 0.1).toInt()}°C) is over $_kBatteryMaxTemperatureInCelsius°C',
);
} else {
healthCheckResult = HealthCheckResult.success(kBatteryTemperatureCheckKey);
}
} on BuildFailedError catch (error) {
healthCheckResult = HealthCheckResult.failure(kBatteryTemperatureCheckKey, error.toString());
}
return healthCheckResult;
}
@override
Future<void> prepareDevices() async {
for (Device device in await discoverDevices()) {
await device.prepare();
}
}
}
class AndroidDevice implements Device {
AndroidDevice({@required this.deviceId});
@override
final String? deviceId;
@override
Future<void> recover() async {
await cleanDevice();
}
@visibleForTesting
Future<void> deletePackageCache() async {
final ProcessResult result = Process.runSync('adb', <String>['shell', 'pm', 'list', 'packages']);
if (result.exitCode != 0) {
throw Exception(result.stderr as String);
}
final packages = <String>[];
// Listen to the stdout and stderr streams
LineSplitter().convert(result.stdout).forEach((data) {
final packageMatch = RegExp(r'package:(.+)$').firstMatch(data);
if (packageMatch != null) {
final packageName = packageMatch.group(1);
if (packageName != null) {
packages.add(packageName);
}
}
});
for (final p in packages) {
final r = Process.runSync('adb', <String>['shell', 'pm', 'clear', p]);
if (r.exitCode != 0) {
print('Clearing package $p resulted in a non-zero exit.');
print('STDERR: ${r.stderr}');
print('STDOUT: ${r.stdout}');
} else {
print('Uninstalling package $p : STDOUT(${r.stdout})');
}
}
}
@visibleForTesting
Future<void> delete3Ppackages() async {
final ProcessResult result = Process.runSync('adb', <String>['shell', 'pm', 'list', 'packages', '-3']);
if (result.exitCode != 0) {
throw Exception(result.stderr as String);
}
final packages = <String>[];
// Listen to the stdout and stderr streams
LineSplitter().convert(result.stdout).forEach((data) {
final packageMatch = RegExp(r'package:(.+)$').firstMatch(data);
if (packageMatch != null) {
final packageName = packageMatch.group(1);
if (packageName != null) {
packages.add(packageName);
}
}
});
for (final p in packages) {
final r = Process.runSync('adb', <String>['shell', 'pm', 'uninstall', p]);
if (r.exitCode != 0) {
print('Uninstalling package $p resulted in a non-zero exit.');
print('STDERR: ${r.stderr}');
print('STDOUT: ${r.stdout}');
} else {
print('Uninstalling package $p : STDOUT(${r.stdout})');
}
}
}
@visibleForTesting
Future<bool> cleanDevice({ProcessManager? processManager}) async {
processManager ??= LocalProcessManager();
final int timeoutSecs = 60;
print('Device recovery: deleting package caches...');
await eval('adb', <String>['wait-for-device'], canFail: false, processManager: processManager)
.timeout(Duration(seconds: timeoutSecs));
await deletePackageCache();
await eval('adb', <String>['wait-for-device'], canFail: false, processManager: processManager)
.timeout(Duration(seconds: timeoutSecs));
print('Device recovery: deleting 3P packages...');
await delete3Ppackages();
await eval('adb', <String>['wait-for-device'], canFail: false, processManager: processManager)
.timeout(Duration(seconds: timeoutSecs));
print('Device recovery: rebooting...');
await eval('adb', <String>['reboot'], canFail: false, processManager: processManager);
return true;
}
@override
Future<void> prepare() async {
await killProcesses();
}
/// Kill top running process if existing.
@visibleForTesting
Future<bool> killProcesses({ProcessManager? processManager}) async {
processManager ??= LocalProcessManager();
String result;
result = await eval(
'adb',
<String>['shell', 'dumpsys', 'activity', '|', 'grep', 'top-activity'],
canFail: true,
processManager: processManager,
);
// Skip uninstalling process when no device is available or no application exists.
if (result == 'adb: no devices/emulators found' || result.isEmpty) {
stdout.write('no process is running');
return true;
}
final List<String> results = result.trim().split('\n');
// Example result:
//
// Proc # 0: fore T/A/T trm: 0 4544:com.google.android.googlequicksearchbox/u0a66 (top-activity)
final List<String> processes =
results.map((result) => result.substring(result.lastIndexOf(':') + 1, result.lastIndexOf('/'))).toList();
try {
for (String process in processes) {
await eval('adb', <String>['shell', 'am', 'force-stop', process], processManager: processManager);
stdout.write('adb stop process: $process');
}
} on BuildFailedError catch (error) {
stderr.write('uninstall applications fails: $error');
return false;
}
return true;
}
}