const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const { extractManifestFromAPK, parseManifest } = require('./manifest'); const { D } = require('./utils/print'); class APKFileInfo { /** * the full file path to the APK file */ fpn = ''; /** * The APK file data * @type {Buffer} */ file_data = null; /** * last modified time of the APK file (in ms) */ app_modified = 0; /** * SHA-1 (hex) digest of the APK file */ content_hash = ''; /** * Contents of Android Manifest XML file */ manifestXml = ''; /** * Extracted data from the manifest */ manifest = { /** * Package name of the app */ package: '', /** * List of all named Activities * @type {string[]} */ activities: [], /** * The launcher Activity */ launcher: '', }; constructor(apk_fpn) { this.fpn = apk_fpn; } /** * Build a new APKFileInfo instance * @param {*} args */ static async from(args) { const result = new APKFileInfo(args.apkFile); // read the APK file contents try { result.file_data = await readFile(args.apkFile); } catch(err) { throw new Error(`APK read error. ${err.message}`); } // save the last modification time of the app result.app_modified = fs.statSync(result.fpn).mtime.getTime(); // create a SHA-1 hash as a simple way to see if we need to install/update the app const h = crypto.createHash('SHA1'); h.update(result.file_data); result.content_hash = h.digest('hex'); // read the manifest try { result.manifestXml = await getAndroidManifestXml(args); } catch (err) { throw new Error(`Manifest read error. ${err.message}`); } // extract the parts we need from the manifest try { result.manifest = parseManifest(result.manifestXml); } catch(err) { throw new Error(`Manifest parse failed. ${err.message}`); } return result; } } /** * Retrieve the AndroidManifest.xml file content * * Because of manifest merging and build-injected properties, the manifest compiled inside * the APK is frequently different from the AndroidManifest.xml source file. * We try to extract the manifest from 3 sources (in priority order): * 1. The 'manifestFile' launch configuration property * 2. The decoded manifest from the APK * 3. The AndroidManifest.xml file from the root of the source tree. */ async function getAndroidManifestXml({manifestFile, apkFile, appSrcRoot}) { let manifest; // a value from the manifestFile overrides the default manifest extraction // note: there's no validation that the file is a valid AndroidManifest.xml file if (manifestFile) { D(`Reading manifest from ${manifestFile}`); manifest = await readFile(manifestFile, 'utf8'); return manifest; } try { D(`Reading APK Manifest`); manifest = await extractManifestFromAPK(apkFile); } catch(err) { // if we fail to get manifest from the APK, revert to the source file version D(`Reading source manifest from ${appSrcRoot} (${err.message})`); manifest = await readFile(path.join(appSrcRoot, 'AndroidManifest.xml'), 'utf8'); } return manifest; } /** * Promisified fs.readFile() * @param {string} path * @param {*} [options] */ function readFile(path, options) { return new Promise((res, rej) => { fs.readFile(path, options || {}, (err, data) => { err ? rej(err) : res(data); }) }) } module.exports = { APKFileInfo, }