Skip to content

Commit

Permalink
chore: Speed up app installation detection (#673)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Jun 22, 2023
1 parent 7c8c7e7 commit dfd8357
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 67 deletions.
62 changes: 37 additions & 25 deletions lib/tools/apk-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,18 @@ const RESOLVER_ACTIVITY_NAME = 'android/com.android.internal.app.ResolverActivit
*
* @param {string} pkg - The name of the package to check.
* @return {Promise<boolean>} True if the package is installed.
* @throws {Error} If there was an error while detecting application state
*/
apkUtilsMethods.isAppInstalled = async function isAppInstalled (pkg) {
log.debug(`Getting install status for ${pkg}`);
const installedPattern = new RegExp(`^\\s*Package\\s+\\[${_.escapeRegExp(pkg)}\\][^:]+:$`, 'm');
let isInstalled;
try {
const stdout = await this.shell(['dumpsys', 'package', pkg]);
const isInstalled = installedPattern.test(stdout);
log.debug(`'${pkg}' is${!isInstalled ? ' not' : ''} installed`);
return isInstalled;
} catch (e) {
throw new Error(`Error finding if '${pkg}' is installed. Original error: ${e.message}`);
const stdout = await this.shell(['pm', 'path', pkg]);
isInstalled = /^package:/m.test(stdout);
} catch (ign) {
isInstalled = false;
}
log.debug(`'${pkg}' is${!isInstalled ? ' not' : ''} installed`);
return isInstalled;
};

/**
Expand Down Expand Up @@ -663,12 +662,15 @@ apkUtilsMethods.getApplicationInstallState = async function getApplicationInstal
return this.APP_INSTALL_STATE.UNKNOWN;
}

if (!await this.isAppInstalled(pkg)) {
const {
versionCode: pkgVersionCode,
versionName: pkgVersionNameStr,
isInstalled,
} = await this.getPackageInfo(pkg);
if (!isInstalled) {
log.debug(`App '${appPath}' is not installed`);
return this.APP_INSTALL_STATE.NOT_INSTALLED;
}

const {versionCode: pkgVersionCode, versionName: pkgVersionNameStr} = await this.getPackageInfo(pkg);
const pkgVersionName = semver.valid(semver.coerce(pkgVersionNameStr));
if (!apkInfo) {
apkInfo = await this.getApkInfo(appPath);
Expand Down Expand Up @@ -1064,8 +1066,9 @@ apkUtilsMethods.setDeviceLanguageCountry = async function setDeviceLanguageCount
/**
* @typedef {Object} AppInfo
* @property {string} name - Package name, for example 'com.acme.app'.
* @property {number} versionCode - Version code.
* @property {string} versionName - Version name, for example '1.0'.
* @property {number?} versionCode - Version code.
* @property {string?} versionName - Version name, for example '1.0'.
* @property {boolean?} isInstalled - true if the app is installed on the device under test.
*/

/**
Expand Down Expand Up @@ -1107,20 +1110,29 @@ apkUtilsMethods.getApkInfo = async function getApkInfo (appPath) {
*/
apkUtilsMethods.getPackageInfo = async function getPackageInfo (pkg) {
log.debug(`Getting package info for '${pkg}'`);
let result = {name: pkg};
const result = {name: pkg};
let stdout;
try {
const stdout = await this.shell(['dumpsys', 'package', pkg]);
const versionNameMatch = new RegExp(/versionName=([\d+.]+)/).exec(stdout);
if (versionNameMatch) {
result.versionName = versionNameMatch[1];
}
const versionCodeMatch = new RegExp(/versionCode=(\d+)/).exec(stdout);
if (versionCodeMatch) {
result.versionCode = parseInt(versionCodeMatch[1], 10);
}
return result;
stdout = await this.shell(['dumpsys', 'package', pkg]);
} catch (err) {
log.warn(`Error '${err.message}' while dumping package info`);
log.debug(err.stack);
log.warn(`Got an unexpected error while dumping package info: ${err.message}`);
return result;
}

const installedPattern = new RegExp(`^\\s*Package\\s+\\[${_.escapeRegExp(pkg)}\\][^:]+:$`, 'm');
result.isInstalled = installedPattern.test(stdout);
if (!result.isInstalled) {
return result;
}

const versionNameMatch = new RegExp(/versionName=([\d+.]+)/).exec(stdout);
if (versionNameMatch) {
result.versionName = versionNameMatch[1];
}
const versionCodeMatch = new RegExp(/versionCode=(\d+)/).exec(stdout);
if (versionCodeMatch) {
result.versionCode = parseInt(versionCodeMatch[1], 10);
}
return result;
};
Expand Down
81 changes: 39 additions & 42 deletions test/unit/apk-utils-specs.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,15 @@ describe('Apk-utils', withMocks({adb, fs, teen_process}, function (mocks) {
it('should parse correctly and return true', async function () {
const pkg = 'dummy.package';
mocks.adb.expects('shell')
.twice().withExactArgs(['dumpsys', 'package', pkg])
.returns(`Packages:
Package [${pkg}] (2469669):
userId=2000`);
.twice().withExactArgs(['pm', 'path', pkg])
.returns(`package:/system/priv-app/TeleService/TeleService.apk`);
(await adb.isAppInstalled(pkg)).should.be.true;
});
it('should parse correctly and return false', async function () {
const pkg = 'dummy.package';
mocks.adb.expects('shell')
.once().withExactArgs(['dumpsys', 'package', pkg])
.returns(`Dexopt state:
Unable to find package: ${pkg}`);
.once().withExactArgs(['pm', 'path', pkg])
.throws();
(await adb.isAppInstalled(pkg)).should.be.false;
});
});
Expand Down Expand Up @@ -811,10 +808,12 @@ describe('Apk-utils', withMocks({adb, fs, teen_process}, function (mocks) {
User 0: ceDataInode=474317 installed=true hidden=false suspended=false stopped=true notLaunched=true enabled=0
runtime permissions:`);
const result = await adb.getPackageInfo('com.example.testapp.first');
for (let [name, value] of [
for (const [name, value] of [
['name', 'com.example.testapp.first'],
['versionCode', 1],
['versionName', '1.0']]) {
['versionName', '1.0'],
['isInstalled', true]
]) {
result.should.have.property(name, value);
}
});
Expand All @@ -825,20 +824,23 @@ describe('Apk-utils', withMocks({adb, fs, teen_process}, function (mocks) {

it('should execute install if the package is not present', async function () {
mocks.adb.expects('getApkInfo').withExactArgs(apkPath).once().returns({
name: pkgId
name: pkgId,
});
mocks.adb.expects('isAppInstalled').withExactArgs(pkgId).once().returns(false);
mocks.adb.expects('getApplicationInstallState').withExactArgs(apkPath, pkgId).once()
.returns(adb.APP_INSTALL_STATE.NOT_INSTALLED);
mocks.adb.expects('install').withArgs(apkPath).once().returns(true);
await adb.installOrUpgrade(apkPath);
});
it('should return if the same package version is already installed', async function () {
mocks.adb.expects('getApkInfo').withExactArgs(apkPath).once().returns({
name: pkgId,
versionCode: 1
});
mocks.adb.expects('getPackageInfo').once().returns({
versionCode: 1
name: pkgId,
versionCode: 1,
isInstalled: true,
});
mocks.adb.expects('isAppInstalled').withExactArgs(pkgId).once().returns(true);
await adb.installOrUpgrade(apkPath, pkgId);
});
it('should return if newer package version is already installed', async function () {
Expand All @@ -847,19 +849,21 @@ describe('Apk-utils', withMocks({adb, fs, teen_process}, function (mocks) {
versionCode: 1
});
mocks.adb.expects('getPackageInfo').once().returns({
versionCode: 2
name: pkgId,
versionCode: 2,
isInstalled: true,
});
mocks.adb.expects('isAppInstalled').withExactArgs(pkgId).once().returns(true);
await adb.installOrUpgrade(apkPath);
});
it('should execute install if apk version code cannot be read', async function () {
mocks.adb.expects('getApkInfo').withExactArgs(apkPath).atLeast(1).returns({
name: pkgId
});
mocks.adb.expects('getPackageInfo').once().returns({
versionCode: 2
name: pkgId,
versionCode: 2,
isInstalled: true,
});
mocks.adb.expects('isAppInstalled').withExactArgs(pkgId).once().returns(true);
mocks.adb.expects('install').withArgs(apkPath).once().returns(true);
await adb.installOrUpgrade(apkPath);
});
Expand All @@ -869,7 +873,6 @@ describe('Apk-utils', withMocks({adb, fs, teen_process}, function (mocks) {
versionCode: 1
});
mocks.adb.expects('getPackageInfo').once().returns({});
mocks.adb.expects('isAppInstalled').withExactArgs(pkgId).once().returns(true);
mocks.adb.expects('install').withArgs(apkPath).once().returns(true);
await adb.installOrUpgrade(apkPath);
});
Expand All @@ -884,9 +887,10 @@ describe('Apk-utils', withMocks({adb, fs, teen_process}, function (mocks) {
versionCode: 2
});
mocks.adb.expects('getPackageInfo').once().returns({
versionCode: 1
name: pkgId,
versionCode: 1,
isInstalled: true,
});
mocks.adb.expects('isAppInstalled').withExactArgs(pkgId).once().returns(true);
mocks.adb.expects('install').withArgs(apkPath, {replace: true}).once().returns(true);
await adb.installOrUpgrade(apkPath);
});
Expand All @@ -897,10 +901,11 @@ describe('Apk-utils', withMocks({adb, fs, teen_process}, function (mocks) {
versionName: '2.0.0',
});
mocks.adb.expects('getPackageInfo').once().returns({
name: pkgId,
versionCode: 1,
versionName: '1.0.0',
isInstalled: true,
});
mocks.adb.expects('isAppInstalled').withExactArgs(pkgId).once().returns(true);
mocks.adb.expects('install').withArgs(apkPath, {replace: true}).once().returns(true);
await adb.installOrUpgrade(apkPath);
});
Expand All @@ -911,10 +916,11 @@ describe('Apk-utils', withMocks({adb, fs, teen_process}, function (mocks) {
versionName: '2.0.0',
});
mocks.adb.expects('getPackageInfo').once().returns({
name: pkgId,
versionCode: 1,
versionName: '2.0.0',
isInstalled: true,
});
mocks.adb.expects('isAppInstalled').withExactArgs(pkgId).once().returns(true);
mocks.adb.expects('install').withArgs(apkPath, {replace: true}).once().returns(true);
await adb.installOrUpgrade(apkPath);
});
Expand All @@ -924,9 +930,10 @@ describe('Apk-utils', withMocks({adb, fs, teen_process}, function (mocks) {
versionCode: 2
});
mocks.adb.expects('getPackageInfo').once().returns({
versionCode: 1
name: pkgId,
versionCode: 1,
isInstalled: true,
});
mocks.adb.expects('isAppInstalled').withExactArgs(pkgId).once().returns(true);
mocks.adb.expects('install').withArgs(apkPath, {replace: true}).once().throws();
mocks.adb.expects('uninstallApk').withExactArgs(pkgId).once().returns(true);
mocks.adb.expects('install').withArgs(apkPath, {replace: false}).once().returns(true);
Expand All @@ -938,37 +945,27 @@ describe('Apk-utils', withMocks({adb, fs, teen_process}, function (mocks) {
versionCode: 2
});
mocks.adb.expects('getPackageInfo').once().returns({
versionCode: 1
name: pkgId,
versionCode: 1,
isInstalled: true,
});
mocks.adb.expects('isAppInstalled').withExactArgs(pkgId).once().returns(true);
mocks.adb.expects('uninstallApk').withExactArgs(pkgId).once().returns(true);
mocks.adb.expects('install').withArgs(apkPath).twice().throws();
let isExceptionThrown = false;
try {
await adb.installOrUpgrade(apkPath);
} catch (e) {
isExceptionThrown = true;
}
isExceptionThrown.should.be.true;
await adb.installOrUpgrade(apkPath).should.be.rejected;
});
it('should throw an exception if upgrade and uninstall fail', async function () {
mocks.adb.expects('getApkInfo').withExactArgs(apkPath).atLeast(1).returns({
name: pkgId,
versionCode: 2
});
mocks.adb.expects('getPackageInfo').once().returns({
versionCode: 1
name: pkgId,
versionCode: 1,
isInstalled: true,
});
mocks.adb.expects('isAppInstalled').withExactArgs(pkgId).once().returns(true);
mocks.adb.expects('uninstallApk').withExactArgs(pkgId).once().returns(false);
mocks.adb.expects('install').withArgs(apkPath).once().throws();
let isExceptionThrown = false;
try {
await adb.installOrUpgrade(apkPath);
} catch (e) {
isExceptionThrown = true;
}
isExceptionThrown.should.be.true;
await adb.installOrUpgrade(apkPath).should.be.rejected;
});
});
describe('dumpsys', function () {
Expand Down

0 comments on commit dfd8357

Please sign in to comment.