| OLD | NEW |
| (Empty) | |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 library android; |
| 6 |
| 7 import "dart:io"; |
| 8 import "dart:async"; |
| 9 import "dart:core"; |
| 10 import "dart:utf"; |
| 11 |
| 12 import "utils.dart"; |
| 13 |
| 14 Future _executeCommand(String executable, |
| 15 List<String> args, |
| 16 [String stdin = ""]) { |
| 17 return _executeCommandRaw(executable, args, stdin).then((results) => null); |
| 18 } |
| 19 |
| 20 Future _executeCommandGetOutput(String executable, |
| 21 List<String> args, |
| 22 [String stdin = ""]) { |
| 23 return _executeCommandRaw(executable, args, stdin) |
| 24 .then((output) => output); |
| 25 } |
| 26 |
| 27 /** |
| 28 * [_executeCommandRaw] will write [stdin] to the standard input of the created |
| 29 * process and will return a tuple (stdout, stderr). |
| 30 * |
| 31 * If the exit code of the process was nonzero it will complete with an error. |
| 32 * If starting the process failed, it will complete with an error as well. |
| 33 */ |
| 34 Future _executeCommandRaw(String executable, |
| 35 List<String> args, |
| 36 [String stdin = ""]) { |
| 37 Future<String> getOutput(Stream<List<int>> stream) { |
| 38 return stream.transform(new StringDecoder()) |
| 39 .reduce(new StringBuffer(), (buf, data) { |
| 40 buf.write(data); |
| 41 return buf; |
| 42 }).then((buf) => buf.toString()); |
| 43 } |
| 44 |
| 45 DebugLogger.info("Running: '\$ $executable ${args.join(' ')}'"); |
| 46 return Process.start(executable, args).then((Process process) { |
| 47 if (stdin != null && stdin != '') { |
| 48 process.stdin.write(stdin); |
| 49 } |
| 50 process.stdin.close(); |
| 51 |
| 52 var futures = [getOutput(process.stdout), |
| 53 getOutput(process.stderr), |
| 54 process.exitCode]; |
| 55 return Future.wait(futures).then((results) { |
| 56 bool success = results[2] == 0; |
| 57 if (!success) { |
| 58 var error = "Running: '\$ $executable ${args.join(' ')}' failed:" |
| 59 "stdout: \n ${results[0]}" |
| 60 "stderr: \n ${results[1]}" |
| 61 "exitCode: \n ${results[2]}"; |
| 62 throw new Exception(error); |
| 63 } |
| 64 return results[0]; |
| 65 }); |
| 66 }); |
| 67 } |
| 68 |
| 69 /** |
| 70 * Helper class to loop through all adb ports. |
| 71 * |
| 72 * The ports come in pairs: |
| 73 * - even number: console connection |
| 74 * - odd number: adb connection |
| 75 * Note that this code doesn't check if the ports are used. |
| 76 */ |
| 77 class AdbServerPortPool { |
| 78 static int MIN_PORT = 5554; |
| 79 static int MAX_PORT = 5584; |
| 80 |
| 81 static int _nextPort = MIN_PORT; |
| 82 |
| 83 static int next() { |
| 84 var port = _nextPort; |
| 85 if (port > MAX_PORT) { |
| 86 throw new Exception("All ports are used."); |
| 87 } |
| 88 _nextPort += 2; |
| 89 return port; |
| 90 } |
| 91 } |
| 92 |
| 93 /** |
| 94 * Represents the interface to the emulator. |
| 95 * New emulators can be launched by calling the static [launchNewEmulator] |
| 96 * method. |
| 97 */ |
| 98 class AndroidEmulator { |
| 99 int _port; |
| 100 Process _emulatorProcess; |
| 101 AdbDevice _adbDevice; |
| 102 |
| 103 int get port => _port; |
| 104 |
| 105 AdbDevice get adbDevice => _adbDevice; |
| 106 |
| 107 static Future<AndroidEmulator> launchNewEmulator(String avdName) { |
| 108 var portNumber = AdbServerPortPool.next(); |
| 109 var args = ['-avd', '$avdName', '-port', "$portNumber" /*, '-gpu', 'on'*/]; |
| 110 return Process.start("emulator64-arm", args).then((Process process) { |
| 111 var adbDevice = new AdbDevice('emulator-$portNumber'); |
| 112 return new AndroidEmulator._private(portNumber, adbDevice, process); |
| 113 }); |
| 114 } |
| 115 |
| 116 AndroidEmulator._private(this._port, this._adbDevice, this._emulatorProcess) { |
| 117 Stream<String> getLines(Stream s) { |
| 118 return s.transform(new StringDecoder()).transform(new LineTransformer()); |
| 119 } |
| 120 |
| 121 getLines(_emulatorProcess.stdout).listen((line) { |
| 122 log("stdout: ${line.trim()}"); |
| 123 }); |
| 124 getLines(_emulatorProcess.stderr).listen((line) { |
| 125 log("stderr: ${line.trim()}"); |
| 126 }); |
| 127 _emulatorProcess.exitCode.then((exitCode) { |
| 128 log("emulator exited with exitCode: $exitCode."); |
| 129 }); |
| 130 } |
| 131 |
| 132 Future<bool> kill() { |
| 133 var completer = new Completer(); |
| 134 if (_emulatorProcess.kill()) { |
| 135 _emulatorProcess.exitCode.then((exitCode) { |
| 136 // TODO: Should we use exitCode to do something clever? |
| 137 completer.complete(true); |
| 138 }); |
| 139 } else { |
| 140 log("Sending kill signal to emulator process failed"); |
| 141 completer.complete(false); |
| 142 } |
| 143 return completer.future; |
| 144 } |
| 145 |
| 146 void log(String msg) { |
| 147 DebugLogger.info("AndroidEmulator(${_adbDevice.deviceId}): $msg"); |
| 148 } |
| 149 } |
| 150 |
| 151 /** |
| 152 * Helper class to create avd device configurations. |
| 153 */ |
| 154 class AndroidHelper { |
| 155 static Future createAvd(String name, String target) { |
| 156 var args = ['--silent', 'create', 'avd', '--name', '$name', |
| 157 '--target', '$target', '--force', '--abi', 'armeabi-v7a']; |
| 158 // We're adding newlines to stdin to simulate <enter>. |
| 159 return _executeCommand("android", args, "\n\n\n\n"); |
| 160 } |
| 161 } |
| 162 |
| 163 /** |
| 164 * Used for communicating with an emulator or with a real device. |
| 165 */ |
| 166 class AdbDevice { |
| 167 static const _adbServerStartupTime = const Duration(seconds: 3); |
| 168 String _deviceId; |
| 169 |
| 170 String get deviceId => _deviceId; |
| 171 |
| 172 AdbDevice(this._deviceId); |
| 173 |
| 174 /** |
| 175 * Blocks execution until the device is online |
| 176 */ |
| 177 Future waitForDevice() { |
| 178 return _adbCommand(['wait-for-device']); |
| 179 } |
| 180 |
| 181 /** |
| 182 * Polls the 'sys.boot_completed' property. Returns as soon as the property is |
| 183 * 1. |
| 184 */ |
| 185 Future waitForBootCompleted() { |
| 186 var timeout = const Duration(seconds: 2); |
| 187 var completer = new Completer(); |
| 188 |
| 189 checkUntilBooted() { |
| 190 _adbCommandGetOutput(['shell', 'getprop', 'sys.boot_completed']) |
| 191 .then((String stdout) { |
| 192 stdout = stdout.trim(); |
| 193 if (stdout == '1') { |
| 194 completer.complete(); |
| 195 } else { |
| 196 new Timer(timeout, checkUntilBooted); |
| 197 } |
| 198 }).catchError((error) { |
| 199 new Timer(timeout, checkUntilBooted); |
| 200 }); |
| 201 } |
| 202 checkUntilBooted(); |
| 203 return completer.future; |
| 204 } |
| 205 |
| 206 /** |
| 207 * Put adb in root mode. |
| 208 */ |
| 209 Future adbRoot() { |
| 210 var adbRootCompleter = new Completer(); |
| 211 return _adbCommand(['root']).then((_) { |
| 212 // TODO: Figure out a way to wait until the adb daemon was restarted in |
| 213 // 'root mode' on the device. |
| 214 new Timer(_adbServerStartupTime, () => adbRootCompleter.complete(true)); |
| 215 }).catchError((error) => adbRootCompleter.completeError(error)); |
| 216 return adbRootCompleter.future; |
| 217 } |
| 218 |
| 219 /** |
| 220 * Download data form the device. |
| 221 */ |
| 222 Future pullData(Path remote, Path local) { |
| 223 return _adbCommand(['pull', '$remote', '$local']); |
| 224 } |
| 225 |
| 226 /** |
| 227 * Upload data to the device. |
| 228 */ |
| 229 Future pushData(Path local, Path remote) { |
| 230 return _adbCommand(['push', '$local', '$remote']); |
| 231 } |
| 232 |
| 233 /** |
| 234 * Change permission of directory recursively. |
| 235 */ |
| 236 Future chmod(String mode, Path directory) { |
| 237 var arguments = ['shell', 'chmod', '-R', mode, '$directory']; |
| 238 return _adbCommand(arguments); |
| 239 } |
| 240 |
| 241 /** |
| 242 * Install an application on the device. |
| 243 */ |
| 244 Future installApk(Path filename) { |
| 245 return _adbCommand( |
| 246 ['install', '-i', 'com.google.android.feedback', '-r', '$filename']); |
| 247 } |
| 248 |
| 249 /** |
| 250 * Start the given intent on the device. |
| 251 */ |
| 252 Future startActivity(Intent intent) { |
| 253 var arguments = ['shell', 'am', 'start', '-W', |
| 254 '-a', intent.action, |
| 255 '-n', "${intent.package}/${intent.activity}"]; |
| 256 if (intent.dataUri != null) { |
| 257 arguments.addAll(['-d', intent.dataUri]); |
| 258 } |
| 259 return _adbCommand(arguments); |
| 260 } |
| 261 |
| 262 /** |
| 263 * Force to stop everything associated with [package]. |
| 264 */ |
| 265 Future forceStop(String package) { |
| 266 var arguments = ['shell', 'am', 'force-stop', package]; |
| 267 return _adbCommand(arguments); |
| 268 } |
| 269 |
| 270 /** |
| 271 * Kill all background processes. |
| 272 */ |
| 273 Future killAll() { |
| 274 var arguments = ['shell', 'am', 'kill-all']; |
| 275 return _adbCommand(arguments); |
| 276 } |
| 277 |
| 278 Future _adbCommand(List<String> adbArgs) { |
| 279 if (_deviceId != null) { |
| 280 var extendedAdbArgs = ['-s', _deviceId]; |
| 281 extendedAdbArgs.addAll(adbArgs); |
| 282 adbArgs = extendedAdbArgs; |
| 283 } |
| 284 return _executeCommand("adb", adbArgs); |
| 285 } |
| 286 |
| 287 Future<String> _adbCommandGetOutput(List<String> adbArgs) { |
| 288 if (_deviceId != null) { |
| 289 var extendedAdbArgs = ['-s', _deviceId]; |
| 290 extendedAdbArgs.addAll(adbArgs); |
| 291 adbArgs = extendedAdbArgs; |
| 292 } |
| 293 return _executeCommandGetOutput("adb", adbArgs); |
| 294 } |
| 295 } |
| 296 |
| 297 /** |
| 298 * Helper to list all adb devicess available. |
| 299 */ |
| 300 class AdbHelper { |
| 301 static RegExp _deviceLineRegexp = |
| 302 new RegExp(r'^([a-zA-Z0-9_-]+)[ \t]+device$', multiLine: true); |
| 303 |
| 304 static Future<List<String>> listDevices() { |
| 305 return Process.run('adb', ['devices']).then((ProcessResult result) { |
| 306 if (result.exitCode != 0) { |
| 307 throw new Exception("Could not list devices [stdout: ${result.stdout}," |
| 308 "stderr: ${result.stderr}]"); |
| 309 } |
| 310 return _deviceLineRegexp.allMatches(result.stdout) |
| 311 .map((Match m) => m.group(1)).toList(); |
| 312 }); |
| 313 } |
| 314 } |
| 315 |
| 316 /** |
| 317 * Represents an android intent. |
| 318 */ |
| 319 class Intent { |
| 320 String action; |
| 321 String package; |
| 322 String activity; |
| 323 String dataUri; |
| 324 |
| 325 Intent(this.action, this.package, this.activity, [this.dataUri]); |
| 326 } |
| 327 |
| OLD | NEW |