├── .gitignore ├── README.md ├── package.json ├── demo.js └── adb.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | adb.js 2 | ====== 3 | 4 | A node.js module which implement pure javascript adb protocol to control Android device -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adb", 3 | "version": "0.2.0", 4 | "main": "./adb.js", 5 | "description": "A node.js module which implement pure javascript adb protocol to control Android device", 6 | "keywords": [ 7 | "android", 8 | "adb" 9 | ], 10 | "author": { 11 | "name": "Flier Lu", 12 | "email": "flier.lu@gmail.com", 13 | "web": "http://flier.lu", 14 | "twitter": "flierlu" 15 | }, 16 | "license": { 17 | "type": "MIT" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "http://github.com/flier/adb.js.git" 22 | }, 23 | "engines": { 24 | "node": "*" 25 | } 26 | } -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | var DebugBridge = require('./adb.js').DebugBridge; 2 | 3 | var adb = new DebugBridge(); 4 | /* 5 | adb.getVersion(function (version) { 6 | console.log('Android Debug Bridge version 1.0.%d', version); 7 | }); 8 | 9 | adb.listDevices(function (devices) { 10 | console.log('found %d device %s', devices.length, devices); 11 | }); 12 | */ 13 | adb.traceDevice(function (devices) { 14 | console.log('found %d device %s', devices.length, devices); 15 | 16 | for (var i=0; i=0; i--) { 118 | buf += '0123456789ABCDEF'.charAt((len >> 4*i) & 0xF); 119 | } 120 | 121 | return buf; 122 | } 123 | 124 | function decodeNumber(str) { 125 | var num = 0; 126 | 127 | for (var i=0; i 4+len) { 180 | session.parseData(data.slice(4+len), callback); 181 | } 182 | } 183 | }; 184 | 185 | DebugSession.prototype.waitCmdResult = function (callback /* (data: buffer) */) { 186 | var session = this; 187 | 188 | this.sock.once('data', function (data) { 189 | console.log('recv %d bytes data: %s', data.length, data); 190 | 191 | session.parseCmdResult(data, callback); 192 | }); 193 | }; 194 | 195 | DebugSession.prototype.recvData = function (callback /* (data: buffer) */) { 196 | this.sock.on('data', callback); 197 | }; 198 | 199 | DebugBridge.prototype.execCommand = function (cmd, callback /* (data: buffer) */, repeat) { 200 | this.connect(function (session) { 201 | session.waitCmdResult(callback, repeat); 202 | session.sendData(cmd); 203 | }); 204 | }; 205 | 206 | DebugBridge.TRANSPORT_USB = 'host:transport-usb'; 207 | DebugBridge.TRANSPORT_LOCAL = 'host:transport-local'; 208 | DebugBridge.TRANSPORT_ANY = 'host:transport-any'; 209 | 210 | DebugBridge.prototype.prepareTransport = function (sn_or_type, callback) { 211 | this.connect(function (session) { 212 | session.waitCmdResult(function (data) { 213 | callback(session); 214 | }); 215 | 216 | var cmd; 217 | 218 | if (sn_or_type.indexOf('host:transport-') == 0) { 219 | cmd = sn_or_type; 220 | } else { 221 | cmd = 'host:transport:' + sn_or_type; 222 | } 223 | 224 | session.sendData(cmd); 225 | }); 226 | }; 227 | 228 | DebugBridge.prototype.getVersion = function (callback /* (version: number) */) { 229 | this.execCommand('host:version', function (data) { 230 | callback(decodeNumber(data)); 231 | }); 232 | }; 233 | 234 | function parseDevices(adb, data) { 235 | var lines = data.toString().split('\n'); 236 | var devices = []; 237 | 238 | for (var i=0; i', this.type, this.id); 273 | }; 274 | 275 | Object.defineProperty(AndroidDevice.prototype, 'isEmulator', { 276 | get: function () { 277 | return this.id.indexOf("emulator-") == 0; 278 | } 279 | }); 280 | 281 | AndroidDevice.prototype.takeSnapshot = function (callback /* (frame: Framebuffer) */) { 282 | this.adb.prepareTransport(this.id, function (session) { 283 | session.waitCmdResult(function (data) { 284 | session.recvData(function (data) { 285 | if (!session.frame) { 286 | session.frame = new AndroidFrame(); 287 | } 288 | 289 | session.frame.parseData(data); 290 | 291 | if (session.frame.isFinished) { 292 | var frame = session.frame; 293 | 294 | session.frame = null; 295 | 296 | callback(frame); 297 | } 298 | }, true); 299 | }); 300 | session.sendData('framebuffer:'); 301 | }); 302 | }; 303 | 304 | AndroidDevice.prototype.getSyncService = function (callback /* (svc: SyncService) */) { 305 | this.adb.prepareTransport(this.id, function (session) { 306 | session.waitCmdResult(function (data) { 307 | callback(new SyncService(session)); 308 | }); 309 | session.sendData('sync:'); 310 | }); 311 | }; 312 | 313 | var AndroidFrame = function () { 314 | }; 315 | 316 | AndroidFrame.prototype.toString = function () { 317 | return util.format('', this.width, this.height, this.depth, this.size); 318 | }; 319 | 320 | Object.defineProperty(AndroidFrame.prototype, 'isFinished', { 321 | get: function () { 322 | return this.pixels && (this.size == this.pixels.length); 323 | } 324 | }); 325 | 326 | function getMask(length) { 327 | return (1 << length) - 1; 328 | } 329 | 330 | AndroidFrame.prototype.convertRGB565toARGB = function (pixels) { 331 | var buf = new Buffer(this.width * this.height * 4); 332 | 333 | for (var x=0; x>> this.red_offset) & getMask(this.red_length)) << (8 - this.red_length); 339 | var g = ((value >>> this.green_offset) & getMask(this.green_length)) << (8 - this.green_length); 340 | var b = ((value >>> this.blue_offset) & getMask(this.blue_length)) << (8 - this.blue_length); 341 | 342 | idx = (y * this.width + x) * 4; 343 | buf[idx++] = b; 344 | buf[idx++] = g; 345 | buf[idx++] = r; 346 | buf[idx] = a; 347 | } 348 | } 349 | 350 | return buf; 351 | }; 352 | 353 | AndroidFrame.prototype.parseFrameHeader = function (data) { 354 | var version = this.pixels.readUInt32LE(0); 355 | 356 | if (version == 1) { 357 | this.depth = this.pixels.readUInt32LE(4); 358 | this.size = this.pixels.readUInt32LE(8); 359 | this.width = this.pixels.readUInt32LE(12); 360 | this.height = this.pixels.readUInt32LE(16); 361 | 362 | // create default values for the rest. Format is 565 363 | this.red_offset = this.pixels.readUInt32LE(20); 364 | this.red_length = this.pixels.readUInt32LE(24); 365 | this.green_offset = this.pixels.readUInt32LE(28); 366 | this.green_length = this.pixels.readUInt32LE(32); 367 | this.blue_offset = this.pixels.readUInt32LE(36); 368 | this.blue_length = this.pixels.readUInt32LE(40); 369 | this.alpha_offset = this.pixels.readUInt32LE(44); 370 | this.alpha_length = this.pixels.readUInt32LE(48); 371 | 372 | this.pixels = this.pixels.slice(13*4); 373 | } else if (version == 16) { 374 | this.depth = 16; 375 | this.size = this.pixels.readUInt32LE(4); 376 | this.width = this.pixels.readUInt32LE(8); 377 | this.height = this.pixels.readUInt32LE(12); 378 | 379 | // create default values for the rest. Format is 565 380 | this.red_offset = 11; 381 | this.red_length = 5; 382 | this.green_offset = 5; 383 | this.green_length = 6; 384 | this.blue_offset = 0; 385 | this.blue_length = 5; 386 | this.alpha_offset = 0; 387 | this.alpha_length = 0; 388 | 389 | this.pixels = this.pixels.slice(16); 390 | } 391 | 392 | console.log('found a %dx%d@%d frame with %d bytes', this.width, this.height, this.depth, this.size); 393 | }; 394 | 395 | AndroidFrame.prototype.parseData = function (data) { 396 | if (this.pixels) { 397 | // console.log("append %d bytes: %s", data.length, data.inspect()); 398 | 399 | this.pixels = Buffer.concat([this.pixels, data], this.pixels.length + data.length); 400 | 401 | if (this.pixels.length == this.size && this.depth == 16) { 402 | this.pixels = this.convertRGB565toARGB(this.pixels); 403 | this.size = this.pixels.length; 404 | } 405 | } else { 406 | this.pixels = data; 407 | } 408 | 409 | if (!this.size && this.pixels.length >= 16) { 410 | this.parseFrameHeader(data); 411 | } 412 | }; 413 | 414 | AndroidFrame.prototype.writeImageFile = function (filename) { 415 | console.log("generating %dx%d image from %d bytes buffer ...", this.width, this.height, this.pixels.length); 416 | 417 | var ext = require('path').extname(filename); 418 | var Image; 419 | 420 | if (ext == '.png') { 421 | Image = require('png').Png; 422 | } else if (ext == '.jpg') { 423 | Image = require('jpeg').Jpeg; 424 | } else if (ext == '.gif') { 425 | Image = require('gif').Gif; 426 | } else { 427 | throw new Error("unknown image type - " + ext); 428 | } 429 | 430 | img = new Image(this.pixels, this.width, this.height, 'rgba'); 431 | 432 | img.encode(function (image, err) { 433 | console.log("writing %d bytes Image file ...", image.length); 434 | 435 | if (err) { throw new Error(err); } 436 | 437 | require('fs').writeFile(filename, image.toString('binary'), 'binary', function (err) { 438 | if (err) { 439 | console.error(err); 440 | } else { 441 | var spawn = require('child_process').spawn; 442 | 443 | spawn('open', [filename]); 444 | } 445 | }); 446 | }); 447 | }; 448 | 449 | var SyncService = function (session) { 450 | this.session = session; 451 | }; 452 | 453 | util.inherits(SyncService, events.EventEmitter); 454 | 455 | SyncService.REMOTE_PATH_MAX_LENGTH = 1024; 456 | 457 | SyncService.ID_OKAY = 'OKAY'; 458 | SyncService.ID_FAIL = 'FAIL'; 459 | SyncService.ID_STAT = 'STAT'; 460 | SyncService.ID_RECV = 'RECV'; 461 | SyncService.ID_DATA = 'DATA'; 462 | SyncService.ID_DONE = 'DONE'; 463 | SyncService.ID_SEND = 'SEND'; 464 | SyncService.ID_LIST = 'LIST'; 465 | SyncService.ID_DENT = 'DENT'; 466 | SyncService.ID_ULNK = 'ULNK'; 467 | SyncService.ID_QUIT = 'QUIT'; 468 | 469 | SyncService.prototype.createFileReq = function (cmd, path) { 470 | var buf = new Buffer(8 + path.length); 471 | 472 | buf.write(cmd, 0, 4, 'binary'); 473 | buf.writeUInt32LE(path.length, 4); 474 | buf.write(path, 8, path.length, 'binary'); 475 | 476 | return buf; 477 | }; 478 | 479 | SyncService.prototype.sendFileReq = function (cmd, path) { 480 | var req = this.createFileReq(cmd, path); 481 | 482 | console.log("send file %s request %s", cmd, req.inspect()); 483 | 484 | this.session.sock.write(req); 485 | }; 486 | 487 | var StreamWriter = function (filename) { 488 | this.stream = require('fs').createWriteStream(filename, { encoding: 'binary'}); 489 | this.ts = new Date().getTime(); 490 | 491 | this.totalSize = 0; 492 | this.chunkSize = 0; 493 | this.closed = false; 494 | }; 495 | 496 | StreamWriter.prototype.close = function () { 497 | this.stream.end(); 498 | this.closed = true; 499 | }; 500 | 501 | StreamWriter.prototype.parseData = function (data) { 502 | if (this.chunkSize > 0) { 503 | var buf = data.length > this.chunkSize ? data.slice(0, this.chunkSize) : data; 504 | var remaining = data.length > this.chunkSize ? data.slice(this.chunkSize) : null; 505 | 506 | this.stream.write(buf); 507 | 508 | this.totalSize += buf.length; 509 | this.chunkSize -= buf.length; 510 | 511 | if (remaining) this.parseData(remaining); 512 | } else { 513 | var id = data.slice(0, 4).toString(); 514 | var size = data.readUInt32LE(4); 515 | 516 | //console.log("found id <%s> with %d bytes", id, size); 517 | 518 | if (id == SyncService.ID_DATA) { 519 | this.chunkSize = size; 520 | 521 | this.parseData(data.slice(8)); 522 | } else if (id == SyncService.ID_DONE) { 523 | console.log("receiving %d bytes file in %d KB/s", this.totalSize, this.recvSpeed); 524 | 525 | this.close(); 526 | } else if (id == SyncService.ID_FAIL) { 527 | var msg = data.slice(8, size).toString(); 528 | 529 | throw new Error('Adb Transfer Protocol Error, ' + msg); 530 | } else { 531 | throw new Error('Adb Transfer Protocol Error, unknown id - ' + id); 532 | } 533 | } 534 | }; 535 | 536 | Object.defineProperty(StreamWriter.prototype, 'recvTimes', { 537 | get: function () { 538 | return new Date().getTime() - this.ts; 539 | } 540 | }); 541 | 542 | Object.defineProperty(StreamWriter.prototype, 'recvSpeed', { 543 | get: function () { 544 | return Math.round(new Number(this.totalSize) * 1000 / this.recvTimes / 1024 * 100) / 100; 545 | } 546 | }); 547 | 548 | SyncService.prototype.pullFile = function (remotePath, localPath, callback /* (size) */) { 549 | if (remotePath.length > SyncService.REMOTE_PATH_MAX_LENGTH) { 550 | throw new Error('Remote path is too long.'); 551 | } 552 | 553 | var writer = new StreamWriter(localPath); 554 | 555 | this.session.sock.on('data', function (data) { 556 | //console.log("recv %d bytes %s", data.length, data.inspect()); 557 | 558 | writer.parseData(data); 559 | 560 | if (writer.closed) callback(writer.totalSize); 561 | }); 562 | 563 | this.sendFileReq(SyncService.ID_RECV, remotePath); 564 | }; --------------------------------------------------------------------------------