From 677a80c16ba78fb96c858e23f3fd39634f34e3e2 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Tue, 19 Jan 2016 11:31:20 -0800 Subject: [PATCH] add a wrapper around the adb command --- .../flutter_tools/lib/src/android/adb.dart | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 packages/flutter_tools/lib/src/android/adb.dart diff --git a/packages/flutter_tools/lib/src/android/adb.dart b/packages/flutter_tools/lib/src/android/adb.dart new file mode 100644 index 00000000000..a0c113dc201 --- /dev/null +++ b/packages/flutter_tools/lib/src/android/adb.dart @@ -0,0 +1,223 @@ +// 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'; +}