// 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; final Map<String, String> _idToNameCache = <String, String>{}; 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(); } /// Listen to device activations and deactivations via the adb server's /// 'track-devices' command. Call cancel on the returned stream to stop /// listening. Stream<List<AdbDevice>> trackDevices() { StreamController<List<AdbDevice>> controller; Socket socket; bool isFirstNotification = true; controller = new StreamController<List<AdbDevice>>( onListen: () async { socket = await Socket.connect(InternetAddress.LOOPBACK_IP_V4, adbServerPort); printTrace('--> host:track-devices'); socket.add(_createAdbRequest('host:track-devices')); socket.listen((List<int> data) async { String stringResult = new String.fromCharCodes(data); printTrace('<-- ${stringResult.trim()}'); _AdbServerResponse response = new _AdbServerResponse( stringResult, noStatus: !isFirstNotification ); String devicesText = response.message.trim(); isFirstNotification = false; if (devicesText.isEmpty) { controller.add(<AdbDevice>[]); } else { List<AdbDevice> devices = devicesText.split('\n').map((String deviceInfo) { return new AdbDevice(deviceInfo); }).where((AdbDevice device) { // Filter unauthorized devices - we can't connect to them. return !device.isUnauthorized && !device.isOffline; }).toList(); await _populateDeviceNames(devices); controller.add(devices); } }); socket.done.then((_) => controller.close()); }, onCancel: () => socket?.destroy() ); return controller.stream; } Future<Null> _populateDeviceNames(List<AdbDevice> devices) async { for (AdbDevice device in devices) { if (device.modelID == null) { // If we don't have a name of a device in our cache, call `device -l` to populate it. if (_idToNameCache[device.id] == null) await _populateDeviceCache(); // Set the device name from the cached name. Adb device notifications only // have IDs, not names. We get the name by calling `listDevices()`. device.modelID = _idToNameCache[device.id]; } } } Future<Null> _populateDeviceCache() async { List<AdbDevice> devices = await listDevices(); for (AdbDevice device in devices) _idToNameCache[device.id] = device.modelID; } 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'; }