// 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/logging.dart'; import '../base/process.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 { static const int adbServerPort = 5037; final String adbPath; Adb(this.adbPath); bool exists() { try { runCheckedSync([adbPath, 'version']); return true; } catch (_) { 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([adbPath, 'version']); /// Starts the adb server. This will throw if there's an problem starting the /// adb server. void startServer() { runCheckedSync([adbPath, 'start-server']); } /// Stops the adb server. This will throw if there's an problem stopping the /// adb server. void killServer() { runCheckedSync([adbPath, 'kill-server']); } /// Ask the ADB server for its internal version number. Future 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> 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 []; return message.split('\n').map( (String deviceInfo) => new AdbDevice(deviceInfo)).toList(); } /// Listen to device activations and deactivations via the asb server's /// 'track-devices' command. Call cancel on the returned stream to stop /// listening. Stream> trackDevices() { StreamController> controller; Socket socket; bool isFirstNotification = true; controller = new StreamController( onListen: () async { socket = await Socket.connect(InternetAddress.LOOPBACK_IP_V4, adbServerPort); logging.fine('--> host:track-devices'); socket.add(_createAdbRequest('host:track-devices')); socket.listen((List data) { String stringResult = new String.fromCharCodes(data); logging.fine('<-- ${stringResult.trim()}'); _AdbServerResponse response = new _AdbServerResponse( stringResult, noStatus: !isFirstNotification ); String devicesText = response.message.trim(); isFirstNotification = false; if (devicesText.isEmpty) { controller.add([]); } else { controller.add(devicesText.split('\n').map((String deviceInfo) { return new AdbDevice(deviceInfo); }).toList()); } }); socket.done.then((_) => controller.close()); }, onCancel: () => socket.destroy() ); return controller.stream; } Future _sendAdbServerCommand(String command) async { Socket socket = await Socket.connect(InternetAddress.LOOPBACK_IP_V4, adbServerPort); try { logging.fine('--> $command'); socket.add(_createAdbRequest(command)); List> result = await socket.toList(); List data = result.fold([], (List previous, List element) { return previous..addAll(element); }); String stringResult = new String.fromCharCodes(data); logging.fine('<-- ${stringResult.trim()}'); return stringResult; } finally { socket.destroy(); } } } class AdbDevice { static final RegExp deviceRegex = new RegExp(r'^(\S+)\s+(\S+)(.*)'); /// Always non-null; something like `TA95000FQA`. String id; /// device, offline, unauthorized. String status; Map _info = {}; 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 = deviceRegex.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 fields = data.split(':'); _info[fields[0]] = fields[1]; } } } } bool get isAvailable => status == 'device'; /// Device model; can be null. `XT1045`, `Nexus_7` String get modelID => _info['model']; /// 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']; operator==(other) => other is AdbDevice && other.id == id; int get hashCode => id.hashCode; String toString() { if (modelID == null) { return '$id ($status)'; } else { return '$id ($status) - $modelID'; } } } List _createAdbRequest(String payload) { List data = payload.codeUnits; // A 4-byte hexadecimal string giving the length of the payload. String prefix = data.length.toRadixString(16).padLeft(4, '0'); List result = new List(); result.addAll(prefix.codeUnits); result.addAll(data); return result; } class _AdbServerResponse { String status; String message; _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); } } bool get isOkay => status == 'OKAY'; bool get isFail => status == 'FAIL'; }