Repository: wix/Detox
Pull request #2188 (2020-07-07 10:51:47)

Test butler app apk
- [ ] This is a small change 
- [ ] This change has been discussed in issue #<?> and the solution has been agreed upon with maintainers.

---

# Description

Add the missing piece in integrating `Test-Butler` into Detox' test app, and with that - enable its integration in _any_ detox project (although the solution isn't pretty, just yet). This in fact is based on the initial work done in #2165.

## More Details

In order for test butler to work, it's not enough to just apply the `aar` into Detox app, because it actually consists of 2 separate parts:
1. A custom, system-level APK that needs to be preinstalled on the tested device.
2. An API that enables / controls it (this is the `aar` part).

_Naturally, without the APK, API usage will fail, and ANR's alert disabling will not be set-up._

With pure Android-native projects, by using the Android-Gradle plugin's `androidTestUtil`, the APK is just automagically preinstalled when running `gradle connectedCheck` (or one of its flavor-suited dependant-tasks). With Detox, we do things ourselves, which makes efficiently installing the APK dynamically, a challange. This PR solves it, then, in a way that is twofold.

###  1. Installing the APK

Assuming the APK is available somewhere in the system, Detox needs to allow for its installation on each running Android device, be it real or an emulator. Note that sometimes the emulator is booted by Detox itself...
Also, we want to install it once and move on, and **not follow** the rules of reinstalling the app, etc.

To work that out, I've allowed for an array of binary paths to be configured under a new field called `utilBinaryPaths` inside Detox' config, that goes alongside the usual things such as `binaryPath` and `testBinaryPath`.

### 2. Downloading the APK

Preferably, the APK should be prebaked into the device image. But that's not always possible (e.g. SaaS CI's). As an alternative, it can be added to the git repo, but that is discourage.

So the fully-blown solution here is to download it manually from bintray. And quite importantly, that should be done only once, before everything starts. Because detox currently does not allow for a global initialization (rather, a distributed, per-run init is done), as an original (yet hackish) solution, I've utilized our fresh ability to create javascript based configuration files (see inside).


noomorph: I have two questions here.

1. A minor one. Could we use an array for `binaryPath` property itself, without creating one more property (`utilBinaryPaths`)? I don't see a good way either (if anything), but I feel it is right to raise this question anyway.
1. A major one. Don't you think that the new behavior is a bit inconsistent with the `reuseApp` flag — the installation seemingly happens in any case? Also, I think I see no way to delete those utility binaries except for if you run `adb uninstall` manually and directly, not via Detox...

🤔 
d4vidi: @noomorph thanks for these super important questions! I think that though there are 2, the answer is one.

As you know, in Detox, we take ownership of various aspect of managing the test devices, as a service to our users. For example, on Android, apart from allocating and booting emulators (which in its own is a pretty big responsibility), we take care of disabling all native animations, for the user's sake.

This feature continues this line. By allowing for _util_ binaries to be set up, we provide yet another means to help users bring their devices to a setup suitable for their environment. To even take this a step further, we could one day consider providing a proper hook where users would have complete access to the allocated devices and tweak whatever they wish in there. But that would be an overkill for what we _currently_ need, I feel.

So in that sense, these util-APK's are not *app* APK's, and they are not to be tested as such. Hence, they are out of scope when it comes to things like `--reuse`, or even app reinstall. In the driver implementation, I even went as far as to precheck whether the APK's have already been installed on the device, so as to avoid reinstalling them (they don't need to be cleaned-up, etc., as they are utils). Uninstalling them doesn't make all that much sense, too - if they're already there, great, it's what we want! :)
Last but not least - again going with the same line of thought here, util binaries therefore exist regardless of whether we support a single _app_ binary, or several ones in `binaryPath`... To me, that's a different matter.

d4vidi: @noomorph that said, I do see a use case that could challenge what I'm suggesting: An update to an existing util APK. This is a case where a user _would_ like to reinstall it. Although, in most cases, the emulators start fresh, and if they don't -- the user should be admin-ing their state.
d4vidi: @noomorph one thing's for sure, this PR screams that we really need to refactor for a global detox setup and teardown -- much more than before.

Patch:
diff --git a/detox/src/Detox.js b/detox/src/Detox.js
index 63b7d7e14..350cf5af8 100644
--- a/detox/src/Detox.js
+++ b/detox/src/Detox.js
@@ -175,6 +175,7 @@ class Detox {
       await this.device.uninstallApp();
       await this.device.installApp();
     }
+    await this.device.installUtilBinaries();
 
     if (behaviorConfig.launchApp) {
       await this.device.launchApp({
diff --git a/detox/src/configuration/composeDeviceConfig.js b/detox/src/configuration/composeDeviceConfig.js
index 39edbdcca..22dae7e3f 100644
--- a/detox/src/configuration/composeDeviceConfig.js
+++ b/detox/src/configuration/composeDeviceConfig.js
@@ -1,26 +1,39 @@
 const _ = require('lodash');
 
-/**
- *
- * @param {DetoxConfigErrorBuilder} errorBuilder
- * @param {*} rawDeviceConfig
- * @param {*} cliConfig
- * @returns {*}
- */
-function composeDeviceConfig({ errorBuilder, rawDeviceConfig, cliConfig }) {
+function validateType({ errorBuilder, rawDeviceConfig }) {
   if (!rawDeviceConfig || !rawDeviceConfig.type) {
     throw errorBuilder.missingConfigurationType();
   }
+}
 
+function getValidatedDeviceName({ errorBuilder, rawDeviceConfig, cliConfig }) {
   const device = cliConfig.deviceName || rawDeviceConfig.device || rawDeviceConfig.name;
-
   if (_.isEmpty(device)) {
     throw errorBuilder.missingDeviceProperty();
   }
+  return device;
+}
+
+function validateUtilBinaryPaths({ errorBuilder, rawDeviceConfig }) {
+  if (rawDeviceConfig.utilBinaryPaths && !_.isArray(rawDeviceConfig.utilBinaryPaths)) {
+    throw errorBuilder.malformedUtilBinaryPaths();
+  }
+}
+
+/**
+ *
+ * @param {DetoxConfigErrorBuilder} errorBuilder
+ * @param {*} rawDeviceConfig
+ * @param {*} cliConfig
+ * @returns {*}
+ */
+function composeDeviceConfig({ errorBuilder, rawDeviceConfig, cliConfig }) {
+  validateType({ errorBuilder, rawDeviceConfig });
 
-  rawDeviceConfig.device = device;
+  rawDeviceConfig.device = getValidatedDeviceName({ errorBuilder, rawDeviceConfig, cliConfig });
   delete rawDeviceConfig.name;
 
+  validateUtilBinaryPaths({ errorBuilder, rawDeviceConfig });
   return rawDeviceConfig;
 }
 
diff --git a/detox/src/configuration/configurations.mock.js b/detox/src/configuration/configurations.mock.js
index d4679290d..f99794ced 100644
--- a/detox/src/configuration/configurations.mock.js
+++ b/detox/src/configuration/configurations.mock.js
@@ -74,6 +74,20 @@ const validOneDeviceAndSession = {
   }
 };
 
+const validOneAndroidDevice = {
+  "configurations": {
+    "android.emu.release": {
+      "binaryPath": "android/app/build/outputs/apk/release/app-release.apk",
+      "testBinaryPath": "android/app/build/outputs/apk/release/app-release-androidTest.apk",
+      "utilBinaryPaths": ["android/util-app1.apk", "android/util-app2.apk"],
+      "type": "android.emulator",
+      "device": {
+        "avdName": "Pixel_API_28",
+      }
+    }
+  }
+};
+
 const pathsTests = {
   "session": {
     "server": "ws://localhost:8099",
@@ -101,5 +115,6 @@ module.exports = {
   pluginsFailingResolved,
   invalidDeviceNoBinary,
   validOneDeviceAndSession,
+  validOneAndroidDevice,
   pathsTests,
 };
diff --git a/detox/src/devices/Device.js b/detox/src/devices/Device.js
index fdf7f0274..35928a3f3 100644
--- a/detox/src/devices/Device.js
+++ b/detox/src/devices/Device.js
@@ -1,5 +1,6 @@
 const _ = require('lodash');
 const debug = require('../utils/debug'); // debug utils, leave here even if unused
+const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 
 class Device {
   constructor({ deviceConfig, deviceDriver, emitter, sessionConfig }) {
@@ -180,6 +181,13 @@ class Device {
     await this.deviceDriver.uninstallApp(this._deviceId, _bundleId);
   }
 
+  async installUtilBinaries() {
+    const paths = this._deviceConfig.utilBinaryPaths;
+    if (paths) {
+      await this.deviceDriver.installUtilBinaries(this._deviceId, paths);
+    }
+  }
+
   async reloadReactNative() {
     await this.deviceDriver.reloadReactNative();
   }
diff --git a/detox/src/devices/drivers/DeviceDriverBase.js b/detox/src/devices/drivers/DeviceDriverBase.js
index 62e4b599e..47d56d03e 100644
--- a/detox/src/devices/drivers/DeviceDriverBase.js
+++ b/detox/src/devices/drivers/DeviceDriverBase.js
@@ -63,7 +63,7 @@ class DeviceDriverBase {
     return await Promise.resolve('');
   }
 
-  async installApp() {
+  async installApp(deviceId, binaryPath, testBinaryPath) {
     return await Promise.resolve('');
   }
 
@@ -71,6 +71,10 @@ class DeviceDriverBase {
     return await Promise.resolve('');
   }
 
+  installUtilBinaries() {
+    return Promise.resolve('');
+  }
+
   async deliverPayload(params) {
     return await this.client.deliverPayload(params);
   }
diff --git a/detox/src/devices/drivers/android/AndroidDriver.js b/detox/src/devices/drivers/android/AndroidDriver.js
index da2a3cb8e..628005506 100644
--- a/detox/src/devices/drivers/android/AndroidDriver.js
+++ b/detox/src/devices/drivers/android/AndroidDriver.js
@@ -10,6 +10,7 @@ const ADB = require('./exec/ADB');
 const AAPT = require('./exec/AAPT');
 const APKPath = require('./tools/APKPath');
 const TempFileXfer = require('./tools/TempFileXfer');
+const AppInstallHelper = require('./tools/AppInstallHelper');
 const AppUninstallHelper = require('./tools/AppUninstallHelper');
 const MonitoredInstrumentation = require('./tools/MonitoredInstrumentation');
 const DetoxApi = require('../../../android/espressoapi/Detox');
@@ -38,6 +39,8 @@ class AndroidDriver extends DeviceDriverBase {
     this.adb = new ADB();
     this.aapt = new AAPT();
     this.fileXfer = new TempFileXfer(this.adb);
+    this.appInstallHelper = new AppInstallHelper(this.adb, this.fileXfer);
+    this.appUninstallHelper = new AppUninstallHelper(this.adb);
     this.devicePathBuilder = new AndroidDevicePathBuilder();
 
     this.instrumentation = new MonitoredInstrumentation(this.adb, logger);
@@ -71,8 +74,16 @@ class AndroidDriver extends DeviceDriverBase {
 
   async uninstallApp(deviceId, bundleId) {
     await this.emitter.emit('beforeUninstallApp', { deviceId, bundleId });
-    const uninstallHelper = new AppUninstallHelper(this.adb);
-    await uninstallHelper.uninstall(deviceId, bundleId);
+    await this.appUninstallHelper.uninstall(deviceId, bundleId);
+  }
+
+  async installUtilBinaries(deviceId, paths) {
+    for (const path of paths) {
+      const packageId = await this.getBundleIdFromBinary(path);
+      if (!await this.adb.isPackageInstalled(deviceId, packageId)) {
+        await this.appInstallHelper.install(deviceId, path);
+      }
+    }
   }
 
   async launchApp(deviceId, bundleId, launchArgs, languageAndLocale) {
diff --git a/detox/src/devices/drivers/android/EmulatorDriver.js b/detox/src/devices/drivers/android/EmulatorDriver.js
index bae2b80fa..4f533f618 100644
--- a/detox/src/devices/drivers/android/EmulatorDriver.js
+++ b/detox/src/devices/drivers/android/EmulatorDriver.js
@@ -9,7 +9,6 @@ const EmulatorLauncher = require('./emulator/EmulatorLauncher');
 const EmulatorVersionResolver = require('./emulator/EmulatorVersionResolver');
 const { EmulatorExec } = require('./exec/EmulatorExec');
 const EmulatorTelnet = require('./tools/EmulatorTelnet');
-const AppInstallHelper = require('./tools/AppInstallHelper');
 const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
 const DeviceRegistry = require('../../DeviceRegistry');
 const environment = require('../../../utils/environment');
@@ -73,9 +72,7 @@ class EmulatorDriver extends AndroidDriver {
       binaryPath,
       testBinaryPath,
     } = this._getInstallPaths(_binaryPath, _testBinaryPath);
-
-    const installHelper = new AppInstallHelper(this.adb, this.fileXfer);
-    await installHelper.install(deviceId, binaryPath, testBinaryPath);
+    await this.appInstallHelper.install(deviceId, binaryPath, testBinaryPath);
   }
 
   async cleanup(adbName, bundleId) {
diff --git a/detox/src/devices/drivers/android/tools/AppInstallHelper.js b/detox/src/devices/drivers/android/tools/AppInstallHelper.js
index c702a2d04..1300b9249 100644
--- a/detox/src/devices/drivers/android/tools/AppInstallHelper.js
+++ b/detox/src/devices/drivers/android/tools/AppInstallHelper.js
@@ -6,22 +6,16 @@ class AppInstallHelper {
 
   async install(deviceId, appBinaryPath, testBinaryPath) {
     await this._fileXfer.prepareDestinationDir(deviceId);
-
-    const appBinaryPathOnTarget = this._getTargetBinaryName(false);
-    await this._pushAndInstallBinary(deviceId, appBinaryPath, appBinaryPathOnTarget);
-
-    const testBinaryPathOnTarget = this._getTargetBinaryName(true);
-    await this._pushAndInstallBinary(deviceId, testBinaryPath, testBinaryPathOnTarget);
+    await this._pushAndInstallBinary(deviceId, appBinaryPath, 'Application.apk');
+    if (testBinaryPath) {
+      await this._pushAndInstallBinary(deviceId, testBinaryPath, 'Test.apk');
+    }
   }
 
   async _pushAndInstallBinary(deviceId, binaryPath, binaryFilenameOnTarget) {
     const binaryPathOnTarget = await this._fileXfer.send(deviceId, binaryPath, binaryFilenameOnTarget);
     await this._adb.remoteInstall(deviceId, binaryPathOnTarget);
   }
-
-  _getTargetBinaryName(isTestBinary) {
-    return isTestBinary ? 'Test.apk' : 'Application.apk';
-  }
 }
 
 module.exports = AppInstallHelper;
diff --git a/detox/src/errors/DetoxConfigErrorBuilder.js b/detox/src/errors/DetoxConfigErrorBuilder.js
index 9d6dfa16d..c6e3fb500 100644
--- a/detox/src/errors/DetoxConfigErrorBuilder.js
+++ b/detox/src/errors/DetoxConfigErrorBuilder.js
@@ -87,6 +87,15 @@ class DetoxConfigErrorBuilder {
     });
   }
 
+  malformedUtilBinaryPaths() {
+    return new DetoxConfigError({
+      message: `Invalid type of "utilBinaryPaths" property in detox.configurations["${this.configurationName}"]\nExpected an array of strings of paths.`,
+      hint: `Check that in your Detox config${_atPath(this.filepath)}`,
+      debugInfo: this._focusOnConfiguration(),
+      inspectOptions: { depth: 2 },
+    });
+  }
+
   missingDeviceProperty() {
     return new DetoxConfigError({
       message: `Missing or empty "device" property inside detox.configurations["${this.configurationName}"].\n` +
diff --git a/docs/APIRef.Configuration.md b/docs/APIRef.Configuration.md
index 3489ca28c..1684547d7 100644
--- a/docs/APIRef.Configuration.md
+++ b/docs/APIRef.Configuration.md
@@ -25,8 +25,9 @@ Please find the [Detox example app](/examples/demo-react-native/detox.config.js)
 |Configuration Params|Details|
 |---|---|
 |`type`| Device type, available options are `ios.simulator`, `ios.none`, `android.emulator`, and `android.attached`. |
-|`binaryPath`| Relative path to the ipa/app due to be  tested (make sure you build the app in a project relative path)|
+|`binaryPath`| Relative path to the ipa/app/apk due to be tested (make sure you build the app in a project relative path) |
 |`testBinaryPath`| (optional, Android only): relative path to the test app (apk) |
+|`utilBinaryPaths`| (optional, Android only): An **array** of relative paths of _utility_ app (apk) binary-files to preinstall on the tested devices - once before the test excution begins.<br />Note: these are not effected by various install-lifecycle events, such as launching an app with `device.launchApp({delete: true})`, which reinstalls the app. A good example of why this might come in handy is [Test Butler](https://github.com/linkedin/test-butler). |
 |`device`| Device query, e.g. `{ "byType": "iPhone 11 Pro" }` for iOS simulator or `{ "avdName": "Pixel_2_API_29" }` |
 |`build`| **[optional]** Build command (either `xcodebuild`, `react-native run-ios`, etc...), will be later available through detox CLI tool.|
 
diff --git a/docs/Introduction.Android.md b/docs/Introduction.Android.md
index 2160c746f..23eb45b2a 100644
--- a/docs/Introduction.Android.md
+++ b/docs/Introduction.Android.md
@@ -209,7 +209,7 @@ For full details, refer to [Android's security-config guide](https://developer.a
 
 
 
-## Proguard (Minification)
+### 7. Proguard (Minification)
 
 In apps running [minification using Proguard](https://developer.android.com/studio/build/shrink-code), in order for Detox to work well on release builds, please enable some Detox proguard-configuration rules by applying the custom configuration file on top of your own. Typically, this is defined using the `proguardFiles` statement in the minification-enabled build-type in your `app/build.gradle`:
 
@@ -230,6 +230,55 @@ In apps running [minification using Proguard](https://developer.android.com/stud
 
 
 
+### 8. Test Butler Support (Optional)
+
+If, when [setting up your work environment](Introduction.AndroidDevEnv.md), you've selected Google emulators with an AOSP image as the test target - as recommended, **we strongly encourage** you would also integrate [Test Butler](https://github.com/linkedin/test-butler): in the very least - in order to supress crash & ANR's dialogs. They are a soft spot in UI testing on Android, all around, as - when displayed, they make the UI entirely inaccessible (and thus cause tests to fail in bulks).
+
+Setting Test Butler up for working with Detox is a bit different than explained in their guides. The process, as a whole, is twofold:
+
+1. Preinstalling the test-butler-app APK onto the test device.
+2. Integrating the test-butler-lib into your own test APK, and initializing it in a custom test-runner (as explained).
+
+The library part can be easily acheived as explained there (i.e. by using Gradle's `androidTestImplementation`). Same goes for initialization. As for the APK, the suggested usage of Gradle's `androidTestUtil` is scarce when running with Detox (i.e. non-native instrumentation tests). Here's what to do instead.
+
+#### Solution 1: Prebaked Images
+
+If you have control over the emulators' snapshots, simply download (see test-butler's guide) and install the test-butler APK once (e.g. use `adb install -r -t path/to/test-butler-app.apk`), and save an updated version of the snapshot. This is the best solution.
+
+> Note: you will have to reiterate this if you upgrade to a newer version of Test-Butler, in the future.
+
+#### Solution 2: Dynamic Installation
+
+Assuming you have the APK available in the system, you can dynamically have Detox automatically install it in all of the running target-emulators by utilizing `utilBinaryPaths` in your Detox configuration. Example:
+
+```json
+{
+  "configuration": {
+    "android.emu.release": {
+      "binaryPath": "android/app/build/outputs/apk/fromBin/release/app-fromBin-release.apk",
+      "utilBinaryPaths": ["relative/path/to/test-butler-app-2.1.0.apk"],
+      "build": "...",
+      "type": "android.emulator",
+      ...
+    }
+  }
+}
+```
+
+> Refer to our [configuration guide](APIRef.Configuration.md) for further details on `utilBinaryPaths`.
+
+As per _making_ the APK available - for that, we have no good solution, for the time being (but it's in the works). A few options might be:
+
+a. In a custom script, have it predownloaded from Bintray directly, as suggest in the Test Butler guide. For example (on a Mac / Linux):
+
+```sh
+curl -f -o ./temp/test-butler-app.apk https://linkedin.bintray.com/maven/com/linkedin/testbutler/test-butler-app/2.1.0/test-butler-app-2.1.0.apk`
+```
+
+> Should you decide to go this path, we recommend you add `./temp/test-butler-app.apk` to the relevant `.gitignore`.
+
+b. (Discouraged) Add it to your source control (e.g. git), as part of the repository.
+
 ## Setting Detox up as a compiling dependency
 
 This is an **alternative** to the setup process described under the previous section, on adding Detox as a dependency.
diff --git a/docs/Introduction.AndroidDevEnv.md b/docs/Introduction.AndroidDevEnv.md
index 0845312e4..b608bd1cd 100644
--- a/docs/Introduction.AndroidDevEnv.md
+++ b/docs/Introduction.AndroidDevEnv.md
@@ -1,4 +1,4 @@
-# Setting Up an Android Development and Testing Envrionment
+# Setting Up an Android Development & Testing Envrionment
 
 This guide provides some core practices to follow in setting up a stable, reliable environment for running automated UI tests using Android emulators (using Detox, in particular) -- be it on a personal, _local_ computer, or a powerful CI machine.
 
@@ -41,14 +41,14 @@ If otherwise the version is simply wrong, try these refs for Macs; consider empl
 
 ## Android SDK
 
-If you have Android Studio installed - as most of us do, then the SDK should be available for you somewhere on your machine<sup>*</sup>. However, for CI agents -- possibly running with no GUI, or if you simply don't want the somewhat bloated piece of software on your computer, it is possible to simply download the SDK and tool-set, purely. Both cases are covered in the [Android guide about Android Studio](https://developer.android.com/studio/). For the pure-tools option, refer to the `Command line tools only` section at the bottom.
+If you have Android Studio installed - as most of us do, then the SDK should be available for you somewhere on your machine<sup>*</sup>. However, for CI agents -- possibly running with no GUI, or if you simply don't want the somewhat bloated piece of software on your computer, it is possible to simply download the SDK and tool-set, purely. Both cases are covered in Google's [Android guide about Android Studio](https://developer.android.com/studio/). For the pure-tools option, refer to the `Command line tools only` section at the bottom.
 
 For more help on setting the SDK up, [this guide might be helpful](https://www.androidcentral.com/installing-android-sdk-windows-mac-and-linux-tutorial).
 
 Whatever option you choose, and whichever platform you're running on (Mac, Linux, Windows), we strongly recommend that eventually you would have 2 additional things set up:
 
 * The path to the SDK's root directory is set into the `ANDROID_SDK_ROOT` [environment variable](https://developer.android.com/studio/command-line/variables).
-* The path to the SDK's root directory is set into the global [`PATH`](https://superuser.com/questions/284342/what-are-path-and-other-environment-variables-and-how-can-i-set-or-use-them) on your computer.
+* The path to the SDK's root directory is bundled into the global [`PATH`](https://superuser.com/questions/284342/what-are-path-and-other-environment-variables-and-how-can-i-set-or-use-them) on your computer.
 
 _<sup>* Inspect the content of your `ANDROID_SDK_ROOT` and `ANDROID_HOME` environment variables.</sup>_
 


Test patch:
diff --git a/detox/src/Detox.test.js b/detox/src/Detox.test.js
index d8764c274..85735e16a 100644
--- a/detox/src/Detox.test.js
+++ b/detox/src/Detox.test.js
@@ -158,6 +158,10 @@ describe('Detox', () => {
         });
       });
 
+      it('should install util-binaries', () => {
+        expect(device().installUtilBinaries).toHaveBeenCalled();
+      });
+
       it('should return itself', async () =>
         expect(await detox.init()).toBeInstanceOf(Detox));
     });
diff --git a/detox/src/configuration/composeDeviceConfig.test.js b/detox/src/configuration/composeDeviceConfig.test.js
index 64260704c..30585f33a 100644
--- a/detox/src/configuration/composeDeviceConfig.test.js
+++ b/detox/src/configuration/composeDeviceConfig.test.js
@@ -41,6 +41,11 @@ describe('composeDeviceConfig', () => {
       delete rawDeviceConfig.device;
       expect(compose).toThrowError(errorBuilder.missingDeviceProperty());
     });
+
+    it('should throw if util-binary paths are malformed', () => {
+      rawDeviceConfig.utilBinaryPaths = 'valid/path/not/in/array';
+      expect(compose).toThrowError(errorBuilder.malformedUtilBinaryPaths());
+    });
   });
 
   describe('if a device configuration has the old .name property', () => {
@@ -84,4 +89,3 @@ describe('composeDeviceConfig', () => {
     });
   });
 });
-
diff --git a/detox/src/devices/Device.test.js b/detox/src/devices/Device.test.js
index 545f92e73..2e6a3bb53 100644
--- a/detox/src/devices/Device.test.js
+++ b/detox/src/devices/Device.test.js
@@ -441,25 +441,19 @@ describe('Device', () => {
   describe('installApp()', () => {
     it(`with a custom app path should use custom app path`, async () => {
       const device = validDevice();
-
       await device.installApp('newAppPath');
-
       expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._deviceId, 'newAppPath', device._deviceConfig.testBinaryPath);
     });
 
     it(`with a custom test app path should use custom test app path`, async () => {
       const device = validDevice();
-
       await device.installApp('newAppPath', 'newTestAppPath');
-
       expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._deviceId, 'newAppPath', 'newTestAppPath');
     });
 
     it(`with no args should use the default path given in configuration`, async () => {
       const device = validDevice();
-
       await device.installApp();
-
       expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._deviceId, device._deviceConfig.binaryPath, device._deviceConfig.testBinaryPath);
     });
   });
@@ -467,21 +461,49 @@ describe('Device', () => {
   describe('uninstallApp()', () => {
     it(`with a custom app path should use custom app path`, async () => {
       const device = validDevice();
-
       await device.uninstallApp('newBundleId');
-
       expect(driverMock.driver.uninstallApp).toHaveBeenCalledWith(device._deviceId, 'newBundleId');
     });
 
     it(`with no args should use the default path given in configuration`, async () => {
       const device = validDevice();
-
       await device.uninstallApp();
-
       expect(driverMock.driver.uninstallApp).toHaveBeenCalledWith(device._deviceId, device._bundleId);
     });
   });
 
+  describe('installBinary()', () => {
+    const configurationName = 'android.emu.release';
+    const scheme = configurationsMock.validOneAndroidDevice;
+
+    it('should install the set of util binaries', async () => {
+      const device = schemeDevice(scheme, configurationName);
+      await device.installUtilBinaries();
+      expect(driverMock.driver.installUtilBinaries).toHaveBeenCalledWith(
+        device._deviceId,
+        scheme.configurations[configurationName].utilBinaryPaths
+      );
+    });
+
+    it('should break if driver installation fails', async () => {
+      driverMock.driver.installUtilBinaries.mockRejectedValue(new Error());
+      const device = schemeDevice(scheme, configurationName);
+      try {
+        await device.installUtilBinaries();
+        fail('');
+      } catch (e) {}
+    });
+
+    it('should not install anything if util-binaries havent been configured', async () => {
+      const _scheme = _.cloneDeep(scheme);
+      delete _scheme.configurations[configurationName].utilBinaryPaths;
+
+      const device = schemeDevice(_scheme, configurationName);
+      await device.installUtilBinaries();
+      expect(driverMock.driver.installUtilBinaries).not.toHaveBeenCalled();
+    });
+  });
+
   it(`sendToHome() should pass to device driver`, async () => {
     const device = validDevice();
     await device.sendToHome();
diff --git a/detox/src/devices/drivers/android/AndroidDriver.test.js b/detox/src/devices/drivers/android/AndroidDriver.test.js
index 6017fc15d..ed5efed82 100644
--- a/detox/src/devices/drivers/android/AndroidDriver.test.js
+++ b/detox/src/devices/drivers/android/AndroidDriver.test.js
@@ -11,31 +11,6 @@ describe('Android driver', () => {
   const mockInstrumentationRunning = () => instrumentation.isRunning.mockReturnValue(true);
   const mockInstrumentationDead = () => instrumentation.isRunning.mockReturnValue(false);
 
-  class MockADBClass {
-    constructor() {
-      Object.assign(this, adb);
-    }
-  }
-
-  class MockTempFileXferClass {
-    constructor() {
-      Object.assign(this, fileXfer);
-    }
-  }
-
-  class MockInvocationManagerClass {
-    constructor() {
-      Object.assign(this, invocationManager);
-    }
-  }
-
-  class MockMonitoredInstrumentationClass {
-    constructor(...args) {
-      instrumentation.mockCtor(...args);
-      Object.assign(this, instrumentation);
-    }
-  }
-
   let logger;
   let client;
   let getAbsoluteBinaryPath;
@@ -43,7 +18,6 @@ describe('Android driver', () => {
   let exec;
   let emitter;
   let detoxApi;
-  let instrumentation;
   beforeEach(() => {
     jest.mock('fs', () => ({
       existsSync: jest.fn(),
@@ -95,6 +69,16 @@ describe('Android driver', () => {
 
     jest.mock('../../../android/espressoapi/Detox');
     detoxApi = require('../../../android/espressoapi/Detox');
+  });
+
+  let instrumentation;
+  beforeEach(() => {
+    class MockMonitoredInstrumentationClass {
+      constructor(...args) {
+        instrumentation.mockCtor(...args);
+        Object.assign(this, instrumentation);
+      }
+    }
 
     const MonitoredInstrumentation = jest.genMockFromModule('./tools/MonitoredInstrumentation');
     instrumentation = new MonitoredInstrumentation();
@@ -105,6 +89,12 @@ describe('Android driver', () => {
 
   let adb;
   beforeEach(() => {
+    class MockADBClass {
+      constructor() {
+        Object.assign(this, adb);
+      }
+    }
+
     const ADB = jest.genMockFromModule('./exec/ADB');
     adb = new ADB();
     adb.adbBin = 'ADB binary mock';
@@ -120,16 +110,43 @@ describe('Android driver', () => {
     jest.mock('./exec/ADB', () => MockADBClass);
   });
 
+  let aapt;
+  beforeEach(() => {
+    class MockAAPTClass {
+      constructor() {
+        Object.assign(this, aapt);
+      }
+    }
+
+    const AAPT = jest.genMockFromModule('./exec/AAPT');
+    aapt = new AAPT();
+    jest.mock('./exec/AAPT', () => MockAAPTClass);
+  });
+
   let fileXfer;
   beforeEach(() => {
+    class MockTempFileXferClass {
+      constructor(...args) {
+        fileXfer.mockCtor(...args);
+        Object.assign(this, fileXfer);
+      }
+    }
+
     const TempFilesXfer = jest.genMockFromModule('./tools/TempFileXfer');
     fileXfer = new TempFilesXfer();
+    fileXfer.mockCtor = jest.fn();
     fileXfer.send.mockResolvedValue(mockNotificationDataTargetPath)
     jest.mock('./tools/TempFileXfer', () => MockTempFileXferClass);
   });
 
   let invocationManager;
   beforeEach(() => {
+    class MockInvocationManagerClass {
+      constructor() {
+        Object.assign(this, invocationManager);
+      }
+    }
+
     const InvocationManager = jest.genMockFromModule('../../../invoke').InvocationManager;
     invocationManager = new InvocationManager();
     jest.mock('../../../invoke', () => ({
@@ -137,6 +154,21 @@ describe('Android driver', () => {
     }))
   });
 
+  let appInstallHelper;
+  beforeEach(() => {
+    class MockAppInstallHelperClass {
+      constructor(...args) {
+        appInstallHelper.mockCtor(...args);
+        Object.assign(this, appInstallHelper);
+      }
+    }
+
+    const AppInstallHelper = jest.genMockFromModule('./tools/AppInstallHelper');
+    appInstallHelper = new AppInstallHelper();
+    appInstallHelper.mockCtor = jest.fn();
+    jest.mock('./tools/AppInstallHelper', () => MockAppInstallHelperClass);
+  });
+
   let uut;
   beforeEach(() => {
     const AndroidDriver = require('./AndroidDriver');
@@ -146,6 +178,16 @@ describe('Android driver', () => {
     });
   });
 
+  describe('Initialization', () => {
+    it('should properly create a FileXfer object', async () => {
+      expect(fileXfer.mockCtor).toHaveBeenCalledWith(adb);
+    });
+
+    it('should properly create an app-install helper', async () => {
+      expect(appInstallHelper.mockCtor).toHaveBeenCalledWith(adb, fileXfer);
+    });
+  });
+
   describe('Instrumentation bootstrap', () => {
     it('should init an instrumentation manager', async () => {
       expect(instrumentation.mockCtor).toHaveBeenCalledWith(adb, logger);
@@ -501,6 +543,49 @@ describe('Android driver', () => {
     });
   });
 
+  describe('Util-binaries installation', () => {
+    const binaryPaths = ['path/to/bin1.apk', '/path/to/bin/2.apk'];
+
+    it('should install using an app-install helper', async () => {
+      await uut.installUtilBinaries(deviceId, binaryPaths);
+      expect(appInstallHelper.install).toHaveBeenCalledWith(deviceId, binaryPaths[0]);
+      expect(appInstallHelper.install).toHaveBeenCalledWith(deviceId, binaryPaths[1]);
+    });
+
+    it('should break if one installation fails', async () => {
+      appInstallHelper.install
+        .mockResolvedValueOnce()
+        .mockRejectedValueOnce(new Error())
+        .mockResolvedValueOnce();
+
+      try {
+        await uut.installUtilBinaries(deviceId, binaryPaths);
+        fail();
+      } catch (e) {
+        expect(appInstallHelper.install).toHaveBeenCalledWith(deviceId, binaryPaths[0]);
+        expect(appInstallHelper.install).toHaveBeenCalledWith(deviceId, binaryPaths[1]);
+        expect(appInstallHelper.install).toHaveBeenCalledTimes(2);
+      }
+    });
+
+    it('should not install if already installed', async () => {
+      adb.isPackageInstalled.mockResolvedValueOnce(false).mockResolvedValueOnce(true);
+      await uut.installUtilBinaries(deviceId, binaryPaths);
+      expect(appInstallHelper.install).toHaveBeenCalledWith(deviceId, binaryPaths[0]);
+      expect(appInstallHelper.install).not.toHaveBeenCalledWith(deviceId, binaryPaths[1]);
+    });
+
+    it('should properly check for preinstallation', async () => {
+      const packageId = 'mockPackageId';
+      const binaryPath = 'some/path/file.apk';
+      aapt.getPackageName.mockResolvedValue(packageId);
+
+      await uut.installUtilBinaries(deviceId, [binaryPath]);
+      expect(adb.isPackageInstalled).toHaveBeenCalledWith(deviceId, packageId)
+      expect(aapt.getPackageName).toHaveBeenCalledWith(mockGetAbsoluteBinaryPathImpl(binaryPath));
+    });
+  });
+
   describe('net-port reversing', () => {
     const deviceId = 1010;
     const port = 1337;
diff --git a/detox/src/devices/drivers/android/emulator/FreeEmulatorFinder.test.js b/detox/src/devices/drivers/android/emulator/FreeEmulatorFinder.test.js
index 627636dd8..933f70b68 100644
--- a/detox/src/devices/drivers/android/emulator/FreeEmulatorFinder.test.js
+++ b/detox/src/devices/drivers/android/emulator/FreeEmulatorFinder.test.js
@@ -1,4 +1,4 @@
-describe('emulators lookup helper', () => {
+describe('FreeEmulatorFinder', () => {
   const mockAdb = {};
   const mockAvdName = 'mock-avd';
 
diff --git a/detox/src/devices/drivers/android/tools/AppInstallHelper.test.js b/detox/src/devices/drivers/android/tools/AppInstallHelper.test.js
index 9f70c43e3..9a1acb003 100644
--- a/detox/src/devices/drivers/android/tools/AppInstallHelper.test.js
+++ b/detox/src/devices/drivers/android/tools/AppInstallHelper.test.js
@@ -44,7 +44,7 @@ describe('Android app installation helper', () => {
     expect(fileXfer.send).toHaveBeenCalledWith(deviceId, testBinaryPath, 'Test.apk');
   });
 
-  it('should fail if file push fails', async () => {
+  it('should break if file push fails', async () => {
     fileXfer.send.mockRejectedValue(new Error('mocked error in adb-push'));
 
     try {
@@ -63,7 +63,7 @@ describe('Android app installation helper', () => {
     expect(adb.remoteInstall).toHaveBeenCalledWith(deviceId, '/mocked-final-dir/second.apk');
   });
 
-  it('should fail if remote-install fails', async () => {
+  it('should break if remote-install fails', async () => {
     adb.remoteInstall.mockRejectedValue(new Error('mocked error in remote-install'));
 
     try {
@@ -71,4 +71,15 @@ describe('Android app installation helper', () => {
       fail('expected to throw');
     } catch(err) {}
   });
+
+  it('should allow for an install with no test binary', async () => {
+    fileXfer.send
+      .mockReturnValueOnce('/mocked-final-dir/first.apk')
+      .mockReturnValueOnce('/mocked-final-dir/second.apk');
+
+    await uut.install(deviceId, appBinaryPath, undefined);
+    expect(fileXfer.send).toHaveBeenCalledTimes(1);
+    expect(adb.remoteInstall).toHaveBeenCalledWith(deviceId, '/mocked-final-dir/first.apk');
+    expect(adb.remoteInstall).toHaveBeenCalledTimes(1);
+  });
 });
diff --git a/detox/src/errors/DetoxConfigErrorBuilder.test.js b/detox/src/errors/DetoxConfigErrorBuilder.test.js
index 978f64228..71d167894 100644
--- a/detox/src/errors/DetoxConfigErrorBuilder.test.js
+++ b/detox/src/errors/DetoxConfigErrorBuilder.test.js
@@ -143,6 +143,26 @@ describe('DetoxConfigErrorBuilder', () => {
     });
   });
 
+  describe('.malformedUtilBinaryPaths', () => {
+    beforeEach(() => {
+      build = () => builder.malformedUtilBinaryPaths()
+      builder.setConfigurationName('android.release');
+      builder.setDetoxConfig({
+        configurations: {
+          'android.release': {
+            type: 'android.emulator',
+            utilBinaryPaths: '/valid/path/outside/of/array',
+            device: 'Pixel 4',
+          },
+        },
+      });
+    });
+
+    it('should create an error with specifying the config name', () => {
+      expect(build()).toMatchSnapshot();
+    });
+  });
+
   describe('.missingDeviceProperty', () => {
     beforeEach(() => {
       build = () => builder.missingDeviceProperty();
diff --git a/detox/src/errors/__snapshots__/DetoxConfigErrorBuilder.test.js.snap b/detox/src/errors/__snapshots__/DetoxConfigErrorBuilder.test.js.snap
index 6a107ad5e..c7842785b 100644
--- a/detox/src/errors/__snapshots__/DetoxConfigErrorBuilder.test.js.snap
+++ b/detox/src/errors/__snapshots__/DetoxConfigErrorBuilder.test.js.snap
@@ -29,6 +29,23 @@ exports[`DetoxConfigErrorBuilder .failedToReadConfiguration should create a simp
 EISDIR: illegal operation on a directory, read]
 `;
 
+exports[`DetoxConfigErrorBuilder .malformedUtilBinaryPaths should create an error with specifying the config name 1`] = `
+[DetoxConfigError: Invalid type of "utilBinaryPaths" property in detox.configurations["android.release"]
+Expected an array of strings of paths.
+
+HINT: Check that in your Detox config.
+
+{
+  configurations: {
+    'android.release': {
+      type: 'android.emulator',
+      utilBinaryPaths: '/valid/path/outside/of/array',
+      device: 'Pixel 4'
+    }
+  }
+}]
+`;
+
 exports[`DetoxConfigErrorBuilder .missingBuildScript should create a generic error, if the config location is not known 1`] = `
 [DetoxConfigError: Could not find a build script inside "android.release" configuration.
 
diff --git a/detox/test/.gitignore b/detox/test/.gitignore
index f98e1f23e..ac3b12a6b 100644
--- a/detox/test/.gitignore
+++ b/detox/test/.gitignore
@@ -1,5 +1,7 @@
 *junit.xml
 
+cache/
+
 # OSX
 #
 .DS_Store
diff --git a/detox/test/android/app/src/androidTest/java/com/example/DetoxTest.java b/detox/test/android/app/src/androidTest/java/com/example/DetoxTest.java
index 22d3a5165..61ae85071 100644
--- a/detox/test/android/app/src/androidTest/java/com/example/DetoxTest.java
+++ b/detox/test/android/app/src/androidTest/java/com/example/DetoxTest.java
@@ -1,7 +1,9 @@
 package com.example;
 
 import android.os.Bundle;
+import android.view.Surface;
 
+import com.linkedin.android.testbutler.TestButler;
 import com.wix.detox.Detox;
 
 import org.junit.Rule;
@@ -38,10 +40,17 @@ public class DetoxTest {
 
     @Test
     public void runDetoxTests() {
+        verifyTestButlerReady();
+
         final ActivityTestRule<?> rule = resolveTestRule();
         Detox.runTests(rule);
     }
 
+    private void verifyTestButlerReady() {
+        // This has no effect if test-butler is running. However, if it is not, then unlike TestButler.setup(), it will hard-fail.
+        TestButler.setRotation(Surface.ROTATION_0);
+    }
+
     private ActivityTestRule<?> resolveTestRule() {
         final Bundle arguments = InstrumentationRegistry.getArguments();
         final boolean useSingleTaskActivity = Boolean.parseBoolean(arguments.getString(USE_SINGLE_INSTANCE_ACTIVITY_ARG, "false"));
diff --git a/detox/test/detox.config-raw.json b/detox/test/detox.config-raw.json
new file mode 100644
index 000000000..e8c9aa3c6
--- /dev/null
+++ b/detox/test/detox.config-raw.json
@@ -0,0 +1,86 @@
+{
+  "test-runner": "nyc jest",
+  "runner-config": "e2e/config.js",
+  "behavior": {
+    "init": {
+      "reinstallApp": true,
+      "launchApp": true,
+      "exposeGlobals": true
+    },
+    "cleanup": {
+      "shutdownDevice": false
+    }
+  },
+  "__session": {
+    "server": "ws://localhost:8099",
+    "sessionId": "test"
+  },
+  "artifacts": {
+    "plugins": {
+      "log": "all",
+      "screenshot": {
+        "shouldTakeAutomaticSnapshots": true,
+        "takeWhen": {
+          "testDone": true
+        }
+      },
+      "timeline": "all"
+    }
+  },
+  "configurations": {
+    "ios.sim.debug": {
+      "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/example.app",
+      "build": "set -o pipefail && xcodebuild -workspace ios/example.xcworkspace -UseNewBuildSystem=NO -scheme example_ci -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build -quiet",
+      "type": "ios.simulator",
+      "device": {
+        "type": "iPhone 11 Pro"
+      }
+    },
+    "ios.sim.release": {
+      "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/example.app",
+      "build": "set -o pipefail && export CODE_SIGNING_REQUIRED=NO && export RCT_NO_LAUNCH_PACKAGER=true && xcodebuild -workspace ios/example.xcworkspace -UseNewBuildSystem=NO -scheme example_ci -configuration Release -sdk iphonesimulator -derivedDataPath ios/build -quiet",
+      "type": "ios.simulator",
+      "device": {
+        "type": "iPhone 11 Pro"
+      }
+    },
+    "ios.none": {
+      "binaryPath": "ios",
+      "type": "ios.none",
+      "device": {
+        "type": "iPhone 11 Pro"
+      },
+      "session": {
+        "server": "ws://localhost:8099",
+        "sessionId": "com.wix.detox-example"
+      }
+    },
+    "android.emu.debug": {
+      "binaryPath": "android/app/build/outputs/apk/fromBin/debug/app-fromBin-debug.apk",
+      "utilBinaryPaths": ["./cache/test-butler-app.apk"],
+      "build": "cd android && ./gradlew assembleFromBinDebug assembleFromBinDebugAndroidTest -DtestBuildType=debug && cd ..",
+      "type": "android.emulator",
+      "device": {
+        "avdName": "Pixel_API_28"
+      }
+    },
+    "android.emu.release": {
+      "binaryPath": "android/app/build/outputs/apk/fromBin/release/app-fromBin-release.apk",
+      "utilBinaryPaths": ["./cache/test-butler-app.apk"],
+      "build": "cd android && ./gradlew assembleFromBinRelease assembleFromBinReleaseAndroidTest -DtestBuildType=release && cd ..",
+      "type": "android.emulator",
+      "device": {
+        "avdName": "Pixel_API_28"
+      }
+    },
+    "android.emu.debug.fromSource": {
+      "binaryPath": "android/app/build/outputs/apk/fromSource/debug/app-fromSource-debug.apk",
+      "utilBinaryPaths": ["./cache/test-butler-app.apk"],
+      "build": "cd android && ./gradlew assembleFromSourceDebug assembleFromSourceDebugAndroidTest -DtestBuildType=debug && cd ..",
+      "type": "android.emulator",
+      "device": {
+        "avdName": "Pixel_API_28"
+      }
+    }
+  }
+}
diff --git a/detox/test/detox.config.js b/detox/test/detox.config.js
new file mode 100644
index 000000000..aa853af94
--- /dev/null
+++ b/detox/test/detox.config.js
@@ -0,0 +1,34 @@
+const fs = require('fs-extra');
+const { execSync } = require('child_process');
+const argv = require('minimist')(process.argv.slice(2));
+const rawConfig = require('./detox.config-raw.json');
+
+function resolveSelectedConfiguration() {
+  const configName = argv['c'] || argv['configuration']; // Hack-ish, until we refactor Detox for a true global init
+  return configName && rawConfig.configurations[configName];
+}
+
+function downloadTestButlerAPKIfNeeded() {
+  const version = '2.1.0';
+  const artifactUrl = `https://linkedin.bintray.com/maven/com/linkedin/testbutler/test-butler-app/${version}/test-butler-app-${version}.apk`;
+  const filePath = './cache/test-butler-app.apk';
+  fs.ensureDirSync('./cache');
+  if (!fs.existsSync(filePath)) {
+    console.log('Downloading Test-Butler APK...');
+    execSync(`curl -f -o ${filePath} ${artifactUrl}`);
+  }
+}
+
+function getConfiguration() {
+  try {
+    const actualConfig = resolveSelectedConfiguration();
+    if (actualConfig && actualConfig.type.includes('android')) {
+      downloadTestButlerAPKIfNeeded();
+    }
+  } catch (e) {
+    throw new Error('Failed to download test-butler APK');
+  }
+  return rawConfig;
+}
+
+module.exports = getConfiguration();
diff --git a/detox/test/package.json b/detox/test/package.json
index d49387f7d..083bac461 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -43,89 +43,6 @@
     "nyc": "^15.1.0",
     "pngjs": "^3.4.0"
   },
-  "detox": {
-    "test-runner": "nyc jest",
-    "runner-config": "e2e/config.js",
-    "behavior": {
-      "init": {
-        "reinstallApp": true,
-        "launchApp": true,
-        "exposeGlobals": true
-      },
-      "cleanup": {
-        "shutdownDevice": false
-      }
-    },
-    "__session": {
-      "server": "ws://localhost:8099",
-      "sessionId": "test"
-    },
-    "artifacts": {
-      "plugins": {
-        "log": "all",
-        "screenshot": {
-          "shouldTakeAutomaticSnapshots": true,
-          "takeWhen": {
-            "testDone": true
-          }
-        },
-        "timeline": "all"
-      }
-    },
-    "configurations": {
-      "ios.sim.debug": {
-        "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/example.app",
-        "build": "set -o pipefail && xcodebuild -workspace ios/example.xcworkspace -UseNewBuildSystem=NO -scheme example_ci -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build -quiet",
-        "type": "ios.simulator",
-        "device": {
-          "type": "iPhone 11 Pro"
-        }
-      },
-      "ios.sim.release": {
-        "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/example.app",
-        "build": "set -o pipefail && export CODE_SIGNING_REQUIRED=NO && export RCT_NO_LAUNCH_PACKAGER=true && xcodebuild -workspace ios/example.xcworkspace -UseNewBuildSystem=NO -scheme example_ci -configuration Release -sdk iphonesimulator -derivedDataPath ios/build -quiet",
-        "type": "ios.simulator",
-        "device": {
-          "type": "iPhone 11 Pro"
-        }
-      },
-      "ios.none": {
-        "binaryPath": "ios",
-        "type": "ios.none",
-        "device": {
-          "type": "iPhone 11 Pro"
-        },
-        "session": {
-          "server": "ws://localhost:8099",
-          "sessionId": "com.wix.detox-example"
-        }
-      },
-      "android.emu.debug": {
-        "binaryPath": "android/app/build/outputs/apk/fromBin/debug/app-fromBin-debug.apk",
-        "build": "cd android && ./gradlew assembleFromBinDebug assembleFromBinDebugAndroidTest -DtestBuildType=debug && cd ..",
-        "type": "android.emulator",
-        "device": {
-          "avdName": "Pixel_API_28"
-        }
-      },
-      "android.emu.release": {
-        "binaryPath": "android/app/build/outputs/apk/fromBin/release/app-fromBin-release.apk",
-        "build": "cd android && ./gradlew assembleFromBinRelease assembleFromBinReleaseAndroidTest -DtestBuildType=release && cd ..",
-        "type": "android.emulator",
-        "device": {
-          "avdName": "Pixel_API_28"
-        }
-      },
-      "android.emu.debug.fromSource": {
-        "binaryPath": "android/app/build/outputs/apk/fromSource/debug/app-fromSource-debug.apk",
-        "build": "cd android && ./gradlew assembleFromSourceDebug assembleFromSourceDebugAndroidTest -DtestBuildType=debug && cd ..",
-        "type": "android.emulator",
-        "device": {
-          "avdName": "Pixel_API_28"
-        }
-      }
-    }
-  },
   "jest-junit": {
     "suiteName": "Detox E2E tests",
     "outputName": "e2e-junit.xml",