const fs = require('fs'); const os = require('os'); const path = require('path'); const exec = require('./exec'); const {StringWritable} = require('./tail'); const System = require('./system'); const Command = require('../run/command'); const through = require('through2'); const Sdk = require('./sdk'); const Env = require('./env'); class Android extends Sdk { constructor(version, ndkVersion) { super(Android.ID, version || Android.VERSION); this.ndk_version = ndkVersion || Android.NDK_VERSION; this.repository = 'https://dl.google.com/android/repository'; } get prepared() { try { return fs.existsSync(this.sdkmanager_bin); } catch (e) { return false; } } get link() { return `${this.repository}/commandlinetools-${System.os}-${this.version}_latest.zip`; } get android_home() { return this.path; } get ndk_home() { return path.join(this.path, 'ndk-bundle') } get sdkmanager_bin() { return path.join(this.path, 'cmdline-tools/bin/sdkmanager'); } prepare() { const result = []; result.push(super.prepare(0)); if (!fs.existsSync(path.join(this.ndk_home))) { result.push(this.prepare_ndk()); } return Promise.all(result); } prepare_ndk() { // ToDo: support windows const url = `${this.repository}/android-ndk-${this.ndk_version}-${System.os}-x86_64.zip`; this.log.d('download: *%s*', url); return Sdk.Downloader.download(url, this.ndk_home, 1, true); } sdkmanager(packages) { this.log.i('sdkmanager: *%s*', packages.join(', ')); const installedFile = path.join(this.path, '.installed'); const installed = new Set( fs.existsSync(installedFile) ? fs.readFileSync(installedFile, {encoding: 'utf8'}).split('\n') : [] ); const install = new Set(packages); for (const key of installed) { if (install.has(key)) { install.delete(key); } } if (install.size === 0) { return Promise.resolve(); } const sdkmanagerBin = this.sdkmanager_bin; if (fs.existsSync(sdkmanagerBin)) { fs.chmodSync(sdkmanagerBin, 0o755); } const yes = '(while sleep 3; do echo "y"; done)'; const command = [yes, '|', sdkmanagerBin] .concat(Array.from(install).map(name => `"${name}"`)) .concat([`--sdk_root="${this.path}"`]) .join(' '); return exec('.', command).then(() => { for (const key of install) { installed.add(key); } fs.writeFileSync(installedFile, Array.from(installed).join('\n'), {encoding: 'utf8'}); }); } activate() { Env.set('ANDROID_HOME', this.android_home); Env.set('ANDROID_NDK_HOME', this.ndk_home); Env.set('NDK_HOME', this.ndk_home); } get adbBin() { return path.join(this.path, 'platform-tools/adb'); } adb(args) { return exec('.', [this.adbBin].concat(args).join(' ')).then(data => { for (let line of data.stderr.split('\n')) { if (line.indexOf('Error') > -1) { throw line; } } return data; }); } buildTool(name) { let buildToolsVersion = null; fs.readdirSync(path.join(this.path, 'build-tools')).forEach(file => { buildToolsVersion = file; }); return path.join(this.path, 'build-tools', buildToolsVersion, name); } aapt(args) { const aaptBin = this.buildTool('aapt'); return exec('.', [aaptBin].concat(args).join(' ')); } apk() { const self = this; return through.obj(function(file, enc, callback) { self.aapt(['l', '-a', file.path]).then(data => { let activity = false; for (let line of data.stdout.split('\n')) { if (line.indexOf('package') > -1) { const value = /"(.*?)"/.exec(line); if (value) file.package = value[1] } if (line.indexOf('activity') > -1) { activity = true; } if (activity && line.indexOf('name') > -1) { const value = /"(.*?)"/.exec(line); if (value) { file.activity = value[1]; activity = false; } } } this.push(file); callback(); }); }); } _installApk(filename, pack) { this.log.i('install *%s*', filename); return this.adb(['install', '-r', '-d', filename]) .then(() => { return false; }) .catch(error => { if (error.includes('INSTALL_FAILED_UPDATE_INCOMPATIBLE')) { this.log.w('!INSTALL FAILED UPDATE INCOMPATIBLE!'); return true; } else { throw error; } }) .then(reinstall => { if (reinstall) { this.log.i('uninstall *%s*', pack); return this.adb(['uninstall', pack]).then(() => { this.log.i('install *%s*', filename); return this.adb(['install', '-r', '-d', filename]); }); } }); } install() { const self = this; return through.obj(function(file, enc, callback) { self._installApk(file.path, file.package).then(() => { // ToDo: kludge: delay before start setTimeout(() => { this.push(file); callback(); }, 500); }); }); } start() { const self = this; return through.obj(function(file, enc, callback) { const name = `${file.package}/${file.activity}`; self.log.i('start *%s*', name); self.adb(['shell', 'am', 'start', '-n', name]).then(() => { setTimeout(() => { self.adb(['shell', `"ps | grep ${file.package}"`]).then(result => { file.pid = result.stdout.split(/\s+/)[1]; this.push(file); callback(); }); }, 1000); }); }); } logcat() { const self = this; return through.obj(function(file, enc, callback) { const cmd = `${self.adbBin} logcat --pid ${file.pid}`; const command = new Command(cmd).exec(); this.push(command); command.contents.pipe(new StringWritable(line => { if (line.includes('SDL') && (line.includes('onDestroy()') || line.includes('onStop()'))) { callback(); throw 'Exit'; // ToDo: } })); }); } sign(keystore, keypass) { const self = this; return through.obj(function(file, enc, callback) { self.log.i('sign *%s*', file.path); const filename = path.join(os.tmpdir(), 'tmp.apk'); fs.writeFileSync(filename, file.contents); const cmd = [ self.buildTool('apksigner'), 'sign', '--ks', keystore, '--ks-pass', `pass:${keypass}`, filename ].join(' '); exec('.', cmd).then(() => { file.contents = fs.readFileSync(filename); this.push(file); callback(); }); }); } } Android.ID = 'android'; Android.VERSION_7302050 = '7302050'; Android.VERSION = Android.VERSION_7302050; Android.NDK_VERSION_R15C = 'r15c'; Android.NDK_VERSION = Android.NDK_VERSION_R15C; module.exports = Android;