Skip to content

Commit 6ea9b4e

Browse files
feat: Add support of .aab packages (#750)
1 parent bb15dbd commit 6ea9b4e

File tree

3 files changed

+74
-40
lines changed

3 files changed

+74
-40
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ appium:showGradleLog | Whether to include Gradle log to the regular server logs
6565

6666
Capability Name | Description
6767
--- | ---
68-
appium:app | Full path to the application to be tested (the app must be located on the same machine where the server is running). Only `.apk` application extension is supported. `.aab` files need to be [converted](https://stackoverflow.com/questions/53040047/generate-apk-file-from-aab-file-android-app-bundle) to `.apk` format using [bundletool](https://developer.android.com/studio/command-line/bundletool) first in order to be used with this driver. Could also be an URL to a remote location. If neither of the `app` or `appPackage` capabilities are provided then the driver will fail to start a session. Also, if `app` capability is not provided it is expected that the app under test is already installed on the device under test and `noReset` is equal to `true`.
68+
appium:app | Full path to the application to be tested (the app must be located on the same machine where the server is running). The `.apk` application extension is supported. Since driver version 2.1.0 `.aab` files are supported as well (they get converted to `.apk` format automatically if [bundletool.jar](https://github.com/google/bundletool/releases) could be found in your PATH). For older driver versions `.aab` files need to be [converted](https://stackoverflow.com/questions/53040047/generate-apk-file-from-aab-file-android-app-bundle) manually to `.apk` format using [bundletool](https://developer.android.com/studio/command-line/bundletool) first. Could also be an URL to a remote location. If neither of the `app` or `appPackage` capabilities are provided then the driver will fail to start a session. Also, if `app` capability is not provided it is expected that the app under test is already installed on the device under test and `noReset` is equal to `true`.
6969
appium:appPackage | Application package identifier to be started. If not provided then Espresso will try to detect it automatically from the package provided by the `app` capability. Read [How To Troubleshoot Activities Startup](https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/android/activity-startup.md) for more details
7070
appium:appActivity | Main application activity identifier. If not provided then Espresso will try to detect it automatically from the package provided by the `app` capability. Read [How To Troubleshoot Activities Startup](https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/android/activity-startup.md) for more details
7171
appium:appWaitActivity | Identifier of the first activity that the application invokes. If not provided then equals to `appium:appActivity`. Read [How To Troubleshoot Activities Startup](https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/android/activity-startup.md) for more details

lib/driver.js

+71-37
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,6 @@ const CHROME_NO_PROXY = [
119119
['POST', new RegExp('^/session/[^/]+/se/log')],
120120
];
121121

122-
const APP_EXTENSION = '.apk';
123-
124122

125123
class EspressoDriver extends BaseDriver {
126124
constructor (opts = {}, shouldValidateCaps = true) {
@@ -177,12 +175,12 @@ class EspressoDriver extends BaseDriver {
177175

178176
if (this.isChromeSession) {
179177
if (this.opts.app) {
180-
logger.warn(`
181-
'browserName' capability will be ignored.
182-
Chrome browser cannot be run in Espresso sessions because Espresso automation doesn't have permission to access Chrome.
183-
`);
178+
logger.warn(`'browserName' capability will be ignored`);
179+
logger.warn(`Chrome browser cannot be run in Espresso sessions because Espresso automation doesn't ` +
180+
`have permission to access Chrome`);
184181
} else {
185-
logger.errorAndThrow(`Chrome browser sessions cannot be run in Espresso because Espresso automation doesn't have permission to access Chrome`);
182+
logger.errorAndThrow(`Chrome browser sessions cannot be run in Espresso because Espresso ` +
183+
`automation doesn't have permission to access Chrome`);
186184
}
187185
}
188186

@@ -191,25 +189,41 @@ class EspressoDriver extends BaseDriver {
191189
this.addWipeDataToAvdArgs();
192190
}
193191

192+
this.opts.systemPort = this.opts.systemPort
193+
|| await findAPortNotInUse(SYSTEM_PORT_RANGE[0], SYSTEM_PORT_RANGE[1]);
194+
this.opts.adbPort = this.opts.adbPort || DEFAULT_ADB_PORT;
195+
// get device udid for this session
196+
const {udid, emPort} = await helpers.getDeviceInfoFromCaps(this.opts);
197+
this.opts.udid = udid;
198+
this.opts.emPort = emPort;
199+
// now that we know our java version and device info, we can create our
200+
// ADB instance
201+
this.adb = await androidHelpers.createADB(this.opts);
202+
194203
if (this.opts.app) {
195204
// find and copy, or download and unzip an app url or path
196-
this.opts.app = await this.helpers.configureApp(this.opts.app, APP_EXTENSION);
197-
await this.checkAppPresent();
205+
this.opts.app = await this.helpers.configureApp(this.opts.app, {
206+
onPostProcess: this.onPostConfigureApp.bind(this),
207+
supportedExtensions: ['.apk', '.aab']
208+
});
198209
} else if (this.appOnDevice) {
199210
// the app isn't an actual app file but rather something we want to
200211
// assume is on the device and just launch via the appPackage
201212
logger.info(`App file was not listed, instead we're going to run ` +
202213
`${this.opts.appPackage} directly on the device`);
203-
await this.checkPackagePresent();
214+
if (!await this.adb.isAppInstalled(this.opts.appPackage)) {
215+
logger.errorAndThrow(`Could not find the package '${this.opts.appPackage}' ` +
216+
`installed on the device`);
217+
}
204218
}
205219

206-
this.opts.systemPort = this.opts.systemPort || await findAPortNotInUse(SYSTEM_PORT_RANGE[0], SYSTEM_PORT_RANGE[1]);
207-
this.opts.adbPort = this.opts.adbPort || DEFAULT_ADB_PORT;
208220
await this.startEspressoSession();
209221
return [sessionId, caps];
210222
} catch (e) {
211223
await this.deleteSession();
212-
e.message += '. Check https://github.com/appium/appium-espresso-driver#troubleshooting regarding advanced session startup troubleshooting.';
224+
e.message += `${_.endsWith(e.message, '.') ? '' : '.'} Check ` +
225+
'https://github.com/appium/appium-espresso-driver#troubleshooting ' +
226+
'regarding advanced session startup troubleshooting.';
213227
if (isErrorType(e, errors.SessionNotCreatedError)) {
214228
throw e;
215229
}
@@ -219,6 +233,46 @@ class EspressoDriver extends BaseDriver {
219233
}
220234
}
221235

236+
async onPostConfigureApp ({cachedAppInfo, isUrl, appPath}) {
237+
const presignApp = async (appLocation) => {
238+
if (this.opts.noSign) {
239+
logger.info('Skipping application signing because noSign capability is set to true. ' +
240+
'Having the application under test with improper signature/non-signed will cause ' +
241+
'Espresso automation startup failure.');
242+
} else if (!await this.adb.checkApkCert(appLocation, this.opts.appPackage)) {
243+
await this.adb.sign(appLocation, this.opts.appPackage);
244+
}
245+
};
246+
247+
const isApk = _.endsWith(_.toLower(appPath), '.apk');
248+
// Do not cache .apk if was already present on the local file system
249+
const shouldResultAppPathBeCached = !isApk || (isApk && isUrl);
250+
let pathInCache = null;
251+
let isResultAppPathAlreadyCached = false;
252+
if (_.isPlainObject(cachedAppInfo)) {
253+
const packageHash = await fs.hash(appPath);
254+
if (packageHash === cachedAppInfo.packageHash && await fs.exists(cachedAppInfo.fullPath)) {
255+
isResultAppPathAlreadyCached = true;
256+
pathInCache = cachedAppInfo.fullPath;
257+
}
258+
}
259+
if (!isResultAppPathAlreadyCached) {
260+
if (shouldResultAppPathBeCached) {
261+
pathInCache = isApk ? appPath : await this.adb.extractUniversalApk(appPath);
262+
if (!isApk && isUrl) {
263+
// Clean up the temporarily downloaded .aab package
264+
await fs.rimraf(appPath);
265+
}
266+
await presignApp(pathInCache);
267+
} else if (isApk) {
268+
// It is probably not the best idea to modify the provided app in-place,
269+
// but this is how it was always working
270+
await presignApp(appPath);
271+
}
272+
}
273+
return shouldResultAppPathBeCached ? {appPath: pathInCache} : false;
274+
}
275+
222276
get driverData () {
223277
// TODO fille out resource info here
224278
return {};
@@ -257,15 +311,6 @@ class EspressoDriver extends BaseDriver {
257311
async startEspressoSession () {
258312
logger.info(`EspressoDriver version: ${version}`);
259313

260-
// get device udid for this session
261-
let {udid, emPort} = await helpers.getDeviceInfoFromCaps(this.opts);
262-
this.opts.udid = udid;
263-
this.opts.emPort = emPort;
264-
265-
// now that we know our java version and device info, we can create our
266-
// ADB instance
267-
this.adb = await androidHelpers.createADB(this.opts);
268-
269314
// Read https://github.com/appium/appium-android-driver/pull/461 what happens if ther is no setHiddenApiPolicy for Android P+
270315
if (await this.adb.getApiLevel() >= 28) { // Android P
271316
logger.warn('Relaxing hidden api policy');
@@ -438,13 +483,6 @@ class EspressoDriver extends BaseDriver {
438483
await this.adb.uninstallApk(this.opts.appPackage);
439484
}
440485
if (this.opts.app) {
441-
if (this.opts.noSign) {
442-
logger.info('Skipping application signing because noSign capability is set to true. ' +
443-
'Having the application under test with improper signature/non-signed will cause ' +
444-
'Espresso automation startup failure.');
445-
} else if (!await this.adb.checkApkCert(this.opts.app, this.opts.appPackage)) {
446-
await this.adb.sign(this.opts.app, this.opts.appPackage);
447-
}
448486
await helpers.installApk(this.adb, this.opts);
449487
}
450488
if (this.opts.skipServerInstallation) {
@@ -525,14 +563,6 @@ class EspressoDriver extends BaseDriver {
525563
}
526564
}
527565

528-
// TODO method is duplicated from uiautomator2
529-
async checkAppPresent () {
530-
logger.debug('Checking whether app is actually present');
531-
if (!(await fs.exists(this.opts.app))) {
532-
logger.errorAndThrow(`Could not find app apk at '${this.opts.app}'`);
533-
}
534-
}
535-
536566
async onSettingsUpdate () {
537567
// intentionally do nothing here, since commands.updateSettings proxies
538568
// settings to the espresso server already
@@ -567,6 +597,10 @@ class EspressoDriver extends BaseDriver {
567597
get isChromeSession () {
568598
return helpers.isChromeBrowser(this.opts.browserName);
569599
}
600+
601+
get appOnDevice () {
602+
return !this.opts.app && helpers.isPackageOrBundle(this.opts.appPackage);
603+
}
570604
}
571605

572606
// first add the android-driver commands which we will fall back to

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@
4949
"!.DS_Store"
5050
],
5151
"dependencies": {
52-
"@appium/base-driver": "^8.0.0",
52+
"@appium/base-driver": "^8.2.4",
5353
"@appium/support": "^2.55.3",
5454
"@babel/runtime": "^7.4.3",
55-
"appium-adb": "^8.18.0",
55+
"appium-adb": "^9.1.0",
5656
"appium-android-driver": "^5.0.0",
5757
"asyncbox": "^2.3.1",
5858
"bluebird": "^3.5.0",

0 commit comments

Comments
 (0)