mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
203 lines
6.6 KiB
Dart
203 lines
6.6 KiB
Dart
// Copyright 2016 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:path/path.dart' as path;
|
|
|
|
import 'utils.dart';
|
|
|
|
typedef Future<Adb> AdbGetter();
|
|
|
|
/// Get an instance of [Adb].
|
|
///
|
|
/// See [realAdbGetter] for signature. This can be overwritten for testing.
|
|
AdbGetter adb = realAdbGetter;
|
|
|
|
Adb _currentDevice;
|
|
|
|
/// Picks a random Android device out of connected devices and sets it as
|
|
/// [_currentDevice].
|
|
Future<Null> pickNextDevice() async {
|
|
List<Adb> allDevices =
|
|
(await Adb.deviceIds).map((String id) => new Adb(deviceId: id)).toList();
|
|
|
|
if (allDevices.length == 0) throw 'No Android devices detected';
|
|
|
|
// TODO(yjbanov): filter out and warn about those with low battery level
|
|
_currentDevice = allDevices[new math.Random().nextInt(allDevices.length)];
|
|
}
|
|
|
|
Future<Adb> realAdbGetter() async {
|
|
if (_currentDevice == null) await pickNextDevice();
|
|
return _currentDevice;
|
|
}
|
|
|
|
/// Gets the ID of an unlocked device, unlocking it if necessary.
|
|
// TODO(yjbanov): abstract away iOS from Android.
|
|
Future<String> getUnlockedDeviceId({ bool ios: false }) async {
|
|
if (ios) {
|
|
// We currently do not have a way to lock/unlock iOS devices, or even to
|
|
// pick one out of many. So we pick the first random iPhone and assume it's
|
|
// already unlocked. For now we'll just keep them at minimum screen
|
|
// brightness so they don't drain battery too fast.
|
|
List<String> iosDeviceIds =
|
|
grep('UniqueDeviceID', from: await eval('ideviceinfo', <String>[]))
|
|
.map((String line) => line.split(' ').last)
|
|
.toList();
|
|
|
|
if (iosDeviceIds.isEmpty) throw 'No connected iOS devices found.';
|
|
|
|
return iosDeviceIds.first;
|
|
}
|
|
|
|
Adb device = await adb();
|
|
await device.unlock();
|
|
return device.deviceId;
|
|
}
|
|
|
|
/// Android Debug Bridge (`adb`) client that exposes a subset of functions
|
|
/// relevant to on-device testing.
|
|
class Adb {
|
|
Adb({ this.deviceId });
|
|
|
|
final String deviceId;
|
|
|
|
// Parses information about a device. Example:
|
|
//
|
|
// 015d172c98400a03 device usb:340787200X product:nakasi model:Nexus_7 device:grouper
|
|
static final RegExp _kDeviceRegex = new RegExp(r'^(\S+)\s+(\S+)(.*)');
|
|
|
|
/// Reports connection health for every device.
|
|
static Future<Map<String, HealthCheckResult>> checkDevices() async {
|
|
Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
|
|
for (String deviceId in await deviceIds) {
|
|
try {
|
|
Adb device = new Adb(deviceId: deviceId);
|
|
// Just a smoke test that we can read wakefulness state
|
|
// TODO(yjbanov): also check battery level
|
|
await device._getWakefulness();
|
|
results['android-device-$deviceId'] = new HealthCheckResult.success();
|
|
} catch (e, s) {
|
|
results['android-device-$deviceId'] = new HealthCheckResult.error(e, s);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
/// Kills the `adb` server causing it to start a new instance upon next
|
|
/// command.
|
|
///
|
|
/// Restarting `adb` helps with keeping device connections alive. When `adb`
|
|
/// runs non-stop for too long it loses connections to devices.
|
|
static Future<Null> restart() async {
|
|
await exec(adbPath, <String>['kill-server'], canFail: false);
|
|
}
|
|
|
|
/// List of device IDs visible to `adb`.
|
|
static Future<List<String>> get deviceIds async {
|
|
List<String> output =
|
|
(await eval(adbPath, <String>['devices', '-l'], canFail: false))
|
|
.trim()
|
|
.split('\n');
|
|
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)) {
|
|
Match match = _kDeviceRegex.firstMatch(line);
|
|
|
|
String deviceID = match[1];
|
|
String deviceState = match[2];
|
|
|
|
if (!const <String>['unauthorized', 'offline'].contains(deviceState)) {
|
|
results.add(deviceID);
|
|
}
|
|
} else {
|
|
throw 'Failed to parse device from adb output: $line';
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/// Whether the device is awake.
|
|
Future<bool> isAwake() async {
|
|
return await _getWakefulness() == 'Awake';
|
|
}
|
|
|
|
/// Whether the device is asleep.
|
|
Future<bool> isAsleep() async {
|
|
return await _getWakefulness() == 'Asleep';
|
|
}
|
|
|
|
/// Wake up the device if it is not awake using [togglePower].
|
|
Future<Null> wakeUp() async {
|
|
if (!(await isAwake())) await togglePower();
|
|
}
|
|
|
|
/// Send the device to sleep mode if it is not asleep using [togglePower].
|
|
Future<Null> sendToSleep() async {
|
|
if (!(await isAsleep())) await togglePower();
|
|
}
|
|
|
|
/// Sends `KEYCODE_POWER` (26), which causes the device to toggle its mode
|
|
/// between awake and asleep.
|
|
Future<Null> togglePower() async {
|
|
await shellExec('input', const <String>['keyevent', '26']);
|
|
}
|
|
|
|
/// Unlocks the device by sending `KEYCODE_MENU` (82).
|
|
///
|
|
/// This only works when the device doesn't have a secure unlock pattern.
|
|
Future<Null> unlock() async {
|
|
await wakeUp();
|
|
await shellExec('input', const <String>['keyevent', '82']);
|
|
}
|
|
|
|
/// Retrieves device's wakefulness state.
|
|
///
|
|
/// See: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/PowerManagerInternal.java
|
|
Future<String> _getWakefulness() async {
|
|
String powerInfo = await shellEval('dumpsys', <String>['power']);
|
|
String wakefulness =
|
|
grep('mWakefulness=', from: powerInfo).single.split('=')[1].trim();
|
|
return wakefulness;
|
|
}
|
|
|
|
/// Executes [command] on `adb shell` and returns its exit code.
|
|
Future<Null> shellExec(String command, List<String> arguments,
|
|
{ Map<String, String> env }) async {
|
|
await exec(adbPath, <String>['shell', command]..addAll(arguments),
|
|
env: env, canFail: false);
|
|
}
|
|
|
|
/// Executes [command] on `adb shell` and returns its standard output as a [String].
|
|
Future<String> shellEval(String command, List<String> arguments,
|
|
{ Map<String, String> env }) {
|
|
return eval(adbPath, <String>['shell', command]..addAll(arguments),
|
|
env: env, canFail: false);
|
|
}
|
|
}
|
|
|
|
/// Path to the `adb` executable.
|
|
String get adbPath {
|
|
String androidHome = Platform.environment['ANDROID_HOME'];
|
|
|
|
if (androidHome == null)
|
|
throw 'ANDROID_HOME environment variable missing. This variable must '
|
|
'point to the Android SDK directory containing platform-tools.';
|
|
|
|
File adbPath = file(path.join(androidHome, 'platform-tools/adb'));
|
|
|
|
if (!adbPath.existsSync()) throw 'adb not found at: $adbPath';
|
|
|
|
return adbPath.absolute.path;
|
|
}
|