| // 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 '../base/process.dart'; |
| import '../globals.dart'; |
| |
| // https://android.googlesource.com/platform/system/core/+/android-4.4_r1/adb/OVERVIEW.TXT |
| // https://android.googlesource.com/platform/system/core/+/android-4.4_r1/adb/SERVICES.TXT |
| |
| /// A wrapper around the `adb` command-line tool and the adb server. |
| class Adb { |
| Adb(this.adbPath); |
| |
| static const int adbServerPort = 5037; |
| |
| final String adbPath; |
| |
| bool exists() { |
| try { |
| runCheckedSync(<String>[adbPath, 'version']); |
| return true; |
| } catch (exception) { |
| return false; |
| } |
| } |
| |
| /// Return the full text from `adb version`. E.g., |
| /// |
| /// Android Debug Bridge version 1.0.32 |
| /// Revision eac51f2bb6a8-android |
| /// |
| /// This method throws if `adb version` fails. |
| String getVersion() => runCheckedSync(<String>[adbPath, 'version']); |
| |
| /// Starts the adb server. This will throw if there's an problem starting the |
| /// adb server. |
| void startServer() { |
| runCheckedSync(<String>[adbPath, 'start-server']); |
| } |
| |
| /// Stops the adb server. This will throw if there's an problem stopping the |
| /// adb server. |
| void killServer() { |
| runCheckedSync(<String>[adbPath, 'kill-server']); |
| } |
| |
| /// Ask the ADB server for its internal version number. |
| Future<String> getServerVersion() { |
| return _sendAdbServerCommand('host:version').then((String response) { |
| _AdbServerResponse adbResponse = new _AdbServerResponse(response); |
| if (adbResponse.isOkay) |
| return adbResponse.message; |
| throw adbResponse.message; |
| }); |
| } |
| |
| /// Queries the adb server for the list of connected adb devices. |
| Future<List<AdbDevice>> listDevices() async { |
| String stringResponse = await _sendAdbServerCommand('host:devices-l'); |
| _AdbServerResponse response = new _AdbServerResponse(stringResponse); |
| if (response.isFail) |
| throw response.message; |
| String message = response.message.trim(); |
| if (message.isEmpty) |
| return <AdbDevice>[]; |
| return message.split('\n').map( |
| (String deviceInfo) => new AdbDevice(deviceInfo) |
| ).toList(); |
| } |
| |
| Future<String> _sendAdbServerCommand(String command) async { |
| Socket socket = await Socket.connect(InternetAddress.LOOPBACK_IP_V4, adbServerPort); |
| |
| try { |
| printTrace('--> $command'); |
| socket.add(_createAdbRequest(command)); |
| List<List<int>> result = await socket.toList(); |
| List<int> data = result.fold(<int>[], (List<int> previous, List<int> element) { |
| return previous..addAll(element); |
| }); |
| String stringResult = new String.fromCharCodes(data); |
| printTrace('<-- ${stringResult.trim()}'); |
| return stringResult; |
| } finally { |
| socket.destroy(); |
| } |
| } |
| } |
| |
| class AdbDevice { |
| AdbDevice(String deviceInfo) { |
| // 'TA95000FQA device' |
| // 'TA95000FQA device usb:340787200X product:peregrine_retus model:XT1045 device:peregrine' |
| // '015d172c98400a03 device usb:340787200X product:nakasi model:Nexus_7 device:grouper' |
| |
| Match match = kDeviceRegex.firstMatch(deviceInfo); |
| id = match[1]; |
| status = match[2]; |
| |
| String rest = match[3]; |
| if (rest != null && rest.isNotEmpty) { |
| rest = rest.trim(); |
| for (String data in rest.split(' ')) { |
| if (data.contains(':')) { |
| List<String> fields = data.split(':'); |
| _info[fields[0]] = fields[1]; |
| } |
| } |
| } |
| |
| if (modelID != null) |
| modelID = cleanAdbDeviceName(modelID); |
| } |
| |
| static final RegExp kDeviceRegex = new RegExp(r'^(\S+)\s+(\S+)(.*)'); |
| |
| /// Always non-null; something like `TA95000FQA`. |
| String id; |
| |
| /// device, offline, unauthorized. |
| String status; |
| |
| final Map<String, String> _info = <String, String>{}; |
| |
| bool get isAvailable => status == 'device'; |
| |
| bool get isUnauthorized => status == 'unauthorized'; |
| |
| bool get isOffline => status == 'offline'; |
| |
| /// Device model; can be null. `XT1045`, `Nexus_7` |
| String get modelID => _info['model']; |
| |
| set modelID(String value) { |
| _info['model'] = value; |
| } |
| |
| /// Device code name; can be null. `peregrine`, `grouper` |
| String get deviceCodeName => _info['device']; |
| |
| /// Device product; can be null. `peregrine_retus`, `nakasi` |
| String get productID => _info['product']; |
| |
| @override |
| bool operator ==(dynamic other) { |
| if (identical(this, other)) |
| return true; |
| if (other is! AdbDevice) |
| return false; |
| final AdbDevice typedOther = other; |
| return id == typedOther.id; |
| } |
| |
| @override |
| int get hashCode => id.hashCode; |
| |
| @override |
| String toString() { |
| if (modelID == null) { |
| return '$id ($status)'; |
| } else { |
| return '$id ($status) - $modelID'; |
| } |
| } |
| } |
| |
| final RegExp _whitespaceRegex = new RegExp(r'\s+'); |
| |
| String cleanAdbDeviceName(String name) { |
| // Some emulators use `___` in the name as separators. |
| name = name.replaceAll('___', ', '); |
| |
| // Convert `Nexus_7` / `Nexus_5X` style names to `Nexus 7` ones. |
| name = name.replaceAll('_', ' '); |
| |
| name = name.replaceAll(_whitespaceRegex, ' ').trim(); |
| |
| return name; |
| } |
| |
| List<int> _createAdbRequest(String payload) { |
| List<int> data = payload.codeUnits; |
| |
| // A 4-byte hexadecimal string giving the length of the payload. |
| String prefix = data.length.toRadixString(16).padLeft(4, '0'); |
| List<int> result = <int>[]; |
| result.addAll(prefix.codeUnits); |
| result.addAll(data); |
| return result; |
| } |
| |
| class _AdbServerResponse { |
| _AdbServerResponse(String text, { bool noStatus: false }) { |
| if (noStatus) { |
| message = text; |
| } else { |
| status = text.substring(0, 4); |
| message = text.substring(4); |
| } |
| |
| // Instead of pulling the hex length out of the response (`000C`), we depend |
| // on the incoming text being the full packet. |
| if (message.isNotEmpty) { |
| // Skip over the 4 byte hex length (`000C`). |
| message = message.substring(4); |
| } |
| } |
| |
| String status; |
| String message; |
| |
| bool get isOkay => status == 'OKAY'; |
| |
| bool get isFail => status == 'FAIL'; |
| } |