Repository: wix/Detox
Pull request #2161 (2020-06-28 15:26:47)

Improve visibility/hit testing logic, add userInteractionEnabled==NO special failure case


Patch:
diff --git a/detox/ios/DTXObjectiveCHelpers b/detox/ios/DTXObjectiveCHelpers
index 2c775d3c2..1eb697540 160000
--- a/detox/ios/DTXObjectiveCHelpers
+++ b/detox/ios/DTXObjectiveCHelpers
@@ -1,1 +1,1 @@
-Subproject commit 2c775d3c263e275e85421fc9036da691f2595778
+Subproject commit 1eb69754040e7781397b05cfb58bf1adb2ce13fc
diff --git a/detox/ios/Detox/DetoxAppDelegateProxy.m b/detox/ios/Detox/DetoxAppDelegateProxy.m
index 127d2da3d..94615d928 100644
--- a/detox/ios/Detox/DetoxAppDelegateProxy.m
+++ b/detox/ios/Detox/DetoxAppDelegateProxy.m
@@ -83,9 +83,7 @@ static NSURL* _launchUserActivityDataURL()
 {
 	static dispatch_once_t onceToken;
 	dispatch_once(&onceToken, ^{
-		Method m1 = class_getInstanceMethod(self, @selector(sendEvent:));
-		Method m2 = class_getInstanceMethod(self, @selector(__dtx_sendEvent:));
-		method_exchangeImplementations(m1, m2);
+		DTXSwizzleMethod(self, @selector(sendEvent:), @selector(__dtx_sendEvent:), NULL);
 	});
 }
 
diff --git a/detox/ios/Detox/GREYIdlingResourcePrettyPrint.m b/detox/ios/Detox/GREYIdlingResourcePrettyPrint.m
index 55f05877a..a17b04cec 100644
--- a/detox/ios/Detox/GREYIdlingResourcePrettyPrint.m
+++ b/detox/ios/Detox/GREYIdlingResourcePrettyPrint.m
@@ -57,15 +57,8 @@ static dispatch_queue_t __tarckedObjectsQueue;
 		__tarckedObjectsMapping = [NSMapTable mapTableWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory];
 		__tarckedObjectsQueue = dispatch_queue_create("com.wix.tarckedObjectsQueue", DISPATCH_QUEUE_SERIAL);
 		
-		Method m1 = class_getInstanceMethod(self, @selector(trackState:forObject:));
-		Method m2 = class_getInstanceMethod(self, @selector(_pp__trackState:forObject:));
-		
-		method_exchangeImplementations(m1, m2);
-		
-		m1 = class_getInstanceMethod(self, @selector(untrackState:forObject:));
-		m2 = class_getInstanceMethod(self, @selector(_pp__untrackState:forObject:));
-		
-		method_exchangeImplementations(m1, m2);
+		DTXSwizzleMethod(self, @selector(trackState:forObject:), @selector(_pp__trackState:forObject:), NULL);
+		DTXSwizzleMethod(self, @selector(untrackState:forObject:), @selector(_pp__untrackState:forObject:), NULL);
 	});
 }
 
diff --git a/detox/ios/Detox/Next/Actions/UIScrollView+DetoxActions.m b/detox/ios/Detox/Next/Actions/UIScrollView+DetoxActions.m
index 7a7f3b72f..28cccec6d 100644
--- a/detox/ios/Detox/Next/Actions/UIScrollView+DetoxActions.m
+++ b/detox/ios/Detox/Next/Actions/UIScrollView+DetoxActions.m
@@ -29,9 +29,8 @@
 
 + (void)load
 {
-	Method m1 = class_getInstanceMethod(UIScrollView.class, @selector(_scrollViewWillEndDraggingWithDeceleration:));
-	Method m2 = class_getInstanceMethod(UIScrollView.class, @selector(_dtx_scrollViewWillEndDraggingWithDeceleration:));
-	method_exchangeImplementations(m1, m2);
+	NSError* error;
+	DTXSwizzleMethod(UIScrollView.class, @selector(_scrollViewWillEndDraggingWithDeceleration:), @selector(_dtx_scrollViewWillEndDraggingWithDeceleration:), &error);
 }
 
 - (void)setDtx_disableDecelerationForScroll:(BOOL)dtx_disableDecelerationForScroll
diff --git a/detox/ios/Detox/Next/AppleInternals/DTXAppleInternals.h b/detox/ios/Detox/Next/AppleInternals/DTXAppleInternals.h
index 182134b75..17f222eff 100644
--- a/detox/ios/Detox/Next/AppleInternals/DTXAppleInternals.h
+++ b/detox/ios/Detox/Next/AppleInternals/DTXAppleInternals.h
@@ -263,6 +263,7 @@ IOHIDEventRef IOHIDEventCreateDigitizerFingerEvent(CFAllocatorRef allocator,
  *  @param isTap A boolean to indicate that this is a tap.
  */
 - (void)setIsTap:(BOOL)isTap;
+- (void)_setIsTapToClick:(BOOL)isTap;
 
 /**
  *  Sets location property to @c location. If @c reset is @c NO, the original value of location will
diff --git a/detox/ios/Detox/Next/AppleTouchEvents/UITouch+DTXAdditions.m b/detox/ios/Detox/Next/AppleTouchEvents/UITouch+DTXAdditions.m
index dc0ed6ab4..f92210860 100644
--- a/detox/ios/Detox/Next/AppleTouchEvents/UITouch+DTXAdditions.m
+++ b/detox/ios/Detox/Next/AppleTouchEvents/UITouch+DTXAdditions.m
@@ -28,13 +28,28 @@
 	if (self)
 	{
 		[self setTapCount:1];
-		[self setIsTap:YES];
 		[self setPhase:UITouchPhaseBegan];
 		[self setWindow:window];
 		[self _setLocationInWindow:point resetPrevious:YES];
 		[self setView:[window hitTest:point withEvent:nil]];
-		[self _setIsFirstTouchForView:YES];
 		[self setTimestamp:[[NSProcessInfo processInfo] systemUptime]];
+		
+		if(@available(iOS 14.0, *))
+		{
+			[self _setIsTapToClick:YES];
+			
+			// We modify the touchFlags ivar struct directly.
+			// First entry is _firstTouchForView
+			Ivar flagsIvar = class_getInstanceVariable(object_getClass(self), "_touchFlags");
+			ptrdiff_t touchFlagsOffset = ivar_getOffset(flagsIvar);
+			char *flags = (__bridge void *)self + touchFlagsOffset;
+			*flags = *flags | (char)0x01;
+		}
+		else
+		{
+			[self setIsTap:YES];
+			[self _setIsFirstTouchForView:YES];
+		}
 	}
 	return self;
 }
diff --git a/detox/ios/Detox/Next/Assertions/DTXAssertionHandler.m b/detox/ios/Detox/Next/Assertions/DTXAssertionHandler.m
index 30cb6a29a..fb35f0924 100644
--- a/detox/ios/Detox/Next/Assertions/DTXAssertionHandler.m
+++ b/detox/ios/Detox/Next/Assertions/DTXAssertionHandler.m
@@ -110,7 +110,6 @@ static void __detox_nested_try(void)
 	} viewDescription:viewDescription];
 }
 
-
 + (DTXTestAssertionException*)_exceptionForFailureInMethod:(SEL)selector object:(id)object file:(NSString *)fileName lineNumber:(NSInteger)line viewDescription:(nullable NSDictionary<NSString*, id>*)viewDescription description:(NSString *)format arguments:(va_list)arguments
 {
 	return (id)[DTXTestAssertionException exceptionWithReason:[[NSString alloc] initWithFormat:format arguments:arguments] userInfo:@{
diff --git a/detox/ios/Detox/Next/Utilities/UIView+DetoxUtils.m b/detox/ios/Detox/Next/Utilities/UIView+DetoxUtils.m
index f578db32e..d615882d8 100644
--- a/detox/ios/Detox/Next/Utilities/UIView+DetoxUtils.m
+++ b/detox/ios/Detox/Next/Utilities/UIView+DetoxUtils.m
@@ -11,7 +11,13 @@
 #import "UIWindow+DetoxUtils.h"
 #import "UISlider+DetoxUtils.h"
 
+@interface DTXTouchVisualizerWindow : UIWindow @end
+
+//#ifdef DEBUG
+//#define _DTXPopulateError(errOut) { NSLog(@"🤦‍♂️ %@", errOut); if(error) { *error = (errOut); } }
+//#else
 #define _DTXPopulateError(errOut) if(error) { *error = (errOut); }
+//#endif
 
 DTX_ALWAYS_INLINE
 static NSDictionary* DTXInsetsToDictionary(UIEdgeInsets insets)
@@ -300,7 +306,7 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 	
 	if(CGRectContainsPoint(self.window.bounds, windowActivationPoint) == NO)
 	{
-		NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Point “%@” is outside if window bounds", DTXPointToString(windowActivationPoint)]}];
+		NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Point “%@” is outside of window bounds", DTXPointToString(windowActivationPoint)]}];
 		_DTXPopulateError(err);
 		
 		return NO;
@@ -322,6 +328,13 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 	
 	__block BOOL rv = NO;
 	BOOL isHit = (selector == @selector(dtx_hitTest:withEvent:lookingFor:));
+
+	if(isHit && self.userInteractionEnabled == NO)
+	{
+		_DTXPopulateError([NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: @"View has user interaction disabled (userInteractionEnabled == NO)"}]);
+
+		return NO;
+	}
 	
 	id (*testFunc)(id, SEL, CGPoint, id, id) = (void*)objc_msgSend;
 	
@@ -338,6 +351,11 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 			return;
 		}
 		
+		if([obj isKindOfClass:DTXTouchVisualizerWindow.class])
+		{
+			return;
+		}
+		
 		CGPoint currentWindowActivationPoint = [screen.coordinateSpace convertPoint:screenActivationPoint toCoordinateSpace:obj.coordinateSpace];
 		
 		if(self.window != obj && isHit == NO)
@@ -346,7 +364,7 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 //			[UIImagePNGRepresentation(windowImage) writeToFile:[NSString stringWithFormat:@"/Users/lnatan/Desktop/%@.png", NSStringFromClass(obj.class)] atomically:YES];
 			if([UIView _dtx_isImageTransparent:windowImage] == NO)
 			{
-				NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Window “%@” is not transparent at window point “%@”", obj.dtx_shortDescription, DTXPointToString(currentWindowActivationPoint)]}];
+				NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Window “%@” is above the tested view's window and is not transparent at window point “%@”", obj.dtx_shortDescription, DTXPointToString(currentWindowActivationPoint)]}];
 				_DTXPopulateError(err);
 				
 				//The window is not transparent at the hit point, stop
@@ -363,33 +381,49 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 		
 		UIView* visibleView = testFunc(obj, selector, currentWindowActivationPoint, nil, self);
 		
-		if(self.window != obj && isHit && visibleView != nil)
+		if(self.window != obj && isHit == YES)
 		{
-			NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Another view “%@” hittable in window “%@” at window point “%@”", visibleView.dtx_shortDescription, obj.dtx_shortDescription, DTXPointToString(currentWindowActivationPoint)]}];
-			_DTXPopulateError(err);
-			
-			//We've hit a view in another window
-			rv = NO;
-			*stop = YES;
+			if(visibleView != nil)
+			{
+				NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Another view “%@” is hittable in window “%@” at window point “%@”", visibleView.dtx_shortDescription, obj.dtx_shortDescription, DTXPointToString(currentWindowActivationPoint)]}];
+				_DTXPopulateError(err);
+				
+				//We've hit a view in another window
+				rv = NO;
+				*stop = YES;
+			}
+			return;
 		}
 		
+		NSAssert(self.window == obj, @"Detox logic failure!");
+		*stop = YES;
+		
 		if(visibleView == self || [visibleView isDescendantOfView:self])
 		{
 			rv = YES;
-			*stop = YES;
 		}
 		else
 		{
-			NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Another view “%@” is %@ at window point “%@”", visibleView.dtx_shortDescription, isHit ? @"hittable" : @"visible", DTXPointToString(currentWindowActivationPoint)]}];
-			_DTXPopulateError(err);
+			rv = NO;
+			NSString* str = isHit ? @"hittable" : @"visible";
+			
+			
+			if(visibleView == NO)
+			{
+				NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"No view is %@ at window point “%@”", str, DTXPointToString(windowActivationPoint)]}];
+				_DTXPopulateError(err);
+			}
+			else
+			{
+				NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Another view “%@” is %@ at window point “%@”", visibleView.dtx_shortDescription, str, DTXPointToString(currentWindowActivationPoint)]}];
+				_DTXPopulateError(err);
+			}
+			
+			return;
 		}
 	}];
 	
-	if(rv == NO)
-	{
-		NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"No view is %@ at window point “%@”", isHit ? @"hittable" : @"visible", DTXPointToString(windowActivationPoint)]}];
-		_DTXPopulateError(err);
-	}
+	
 	
 	return rv;
 }
diff --git a/detox/ios/EarlGrey b/detox/ios/EarlGrey
index 45ee035c3..dc658d96e 160000
--- a/detox/ios/EarlGrey
+++ b/detox/ios/EarlGrey
@@ -1,1 +1,1 @@
-Subproject commit 45ee035c341144308e675846ac190ed32da25cc9
+Subproject commit dc658d96e95adfb09b74fc5d52dec33a5fa60531
diff --git a/detox/src/client/actions/actions.js b/detox/src/client/actions/actions.js
index 06b69d8d4..d260a33bc 100644
--- a/detox/src/client/actions/actions.js
+++ b/detox/src/client/actions/actions.js
@@ -113,10 +113,10 @@ class Invoke extends Action {
   async handle(response) {
     switch (response.type) {
       case 'testFailed':
-        throw new Error('Test Failed:' + response.params.details +
+        throw new Error('Test Failed: ' + response.params.details +
           /* istanbul ignore next */
           (log.level() <= bunyan.DEBUG ?
-          '\nView Hierarchy (presented in loglevel verbose and above):\n' + response.params.viewHierarchy :
+          '\nView Hierarchy:\n' + response.params.viewHierarchy :
           '\nTIP: To print view hierarchy on failed actions/matches, use loglevel verbose and above.'));
       case 'invokeResult':
         return response.params;
diff --git a/detox/src/devices/drivers/android/AndroidDriver.js b/detox/src/devices/drivers/android/AndroidDriver.js
index da2a3cb8e..feb3a44ef 100644
--- a/detox/src/devices/drivers/android/AndroidDriver.js
+++ b/detox/src/devices/drivers/android/AndroidDriver.js
@@ -11,7 +11,7 @@ const AAPT = require('./exec/AAPT');
 const APKPath = require('./tools/APKPath');
 const TempFileXfer = require('./tools/TempFileXfer');
 const AppUninstallHelper = require('./tools/AppUninstallHelper');
-const MonitoredInstrumentation = require('./tools/MonitoredInstrumentation');
+const Instrumentation = require('./tools/Instrumentation');
 const DetoxApi = require('../../../android/espressoapi/Detox');
 const EspressoDetoxApi = require('../../../android/espressoapi/EspressoDetox');
 const UiDeviceProxy = require('../../../android/espressoapi/UiDeviceProxy');
@@ -22,10 +22,12 @@ const ADBScreenrecorderPlugin = require('../../../artifacts/video/ADBScreenrecor
 const AndroidDevicePathBuilder = require('../../../artifacts/utils/AndroidDevicePathBuilder');
 const TimelineArtifactPlugin = require('../../../artifacts/timeline/TimelineArtifactPlugin');
 const temporaryPath = require('../../../artifacts/utils/temporaryPath');
+const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
 const sleep = require('../../../utils/sleep');
 const retry = require('../../../utils/retry');
 const getAbsoluteBinaryPath = require('../../../utils/getAbsoluteBinaryPath');
 const AndroidExpect = require('../../../android/expect');
+const { InstrumentationLogsParser } = require('./InstrumentationLogsParser');
 
 class AndroidDriver extends DeviceDriverBase {
   constructor(config) {
@@ -40,7 +42,10 @@ class AndroidDriver extends DeviceDriverBase {
     this.fileXfer = new TempFileXfer(this.adb);
     this.devicePathBuilder = new AndroidDevicePathBuilder();
 
-    this.instrumentation = new MonitoredInstrumentation(this.adb, logger);
+    this.instrumentationLogsParser = null;
+    this.instrumentationStackTrace = '';
+    this.instrumentationCloseListener = _.noop;
+    this.instrumentation = new Instrumentation(this.adb, logger);
   }
 
   declareArtifactPlugins() {
@@ -102,9 +107,15 @@ class AndroidDriver extends DeviceDriverBase {
 
   async waitUntilReady() {
       try {
-        await Promise.race([super.waitUntilReady(), this.instrumentation.waitForCrash()]);
+        await Promise.race([
+          super.waitUntilReady(),
+          new Promise((resolve, reject) => {
+            this.instrumentationCloseListener = () => reject(this._getInstrumentationCrashError());
+            !this.instrumentation.isRunning() && this.instrumentationCloseListener();
+          }),
+        ]);
       } finally {
-        this.instrumentation.abortWaitForCrash();
+        this.instrumentationCloseListener = _.noop;
       }
   }
 
@@ -232,16 +243,30 @@ class AndroidDriver extends DeviceDriverBase {
     const serverPort = new URL(this.client.configuration.server).port;
     await this.adb.reverse(deviceId, serverPort);
 
-    this.instrumentation.setTerminationFn(async () => {
-      await this._terminateInstrumentation();
-      await this.adb.reverseRemove(deviceId, serverPort);
-    });
+    this.instrumentationLogsParser = new InstrumentationLogsParser();
+    this.instrumentation.setTerminationFn(() => this._onInstrumentationTerminated(deviceId, serverPort));
+    this.instrumentation.setLogListenFn(this._extractStackTraceFromInstrumLogs.bind(this));
     await this.instrumentation.launch(deviceId, bundleId, userLaunchArgs);
   }
 
+  async _onInstrumentationTerminated(deviceId, serverPort) {
+    await this.adb.reverseRemove(deviceId, serverPort);
+    this.instrumentationCloseListener();
+    this.instrumentation.clearAllCallbackFn();
+  }
+
   async _terminateInstrumentation() {
     await this.instrumentation.terminate();
-    await this.instrumentation.setTerminationFn(null);
+    this.instrumentationCloseListener();
+    this.instrumentation.clearAllCallbackFn();
+  }
+
+  _extractStackTraceFromInstrumLogs(logsDump) {
+    this.instrumentationLogsParser.parse(logsDump);
+
+    if (this.instrumentationLogsParser.containsStackTraceLog(logsDump)) {
+      this.instrumentationStackTrace = this.instrumentationLogsParser.getStackTrace(logsDump);
+    }
   }
 
   async _sendNotificationDataToDevice(dataFileLocalPath, deviceId) {
@@ -282,6 +307,20 @@ class AndroidDriver extends DeviceDriverBase {
     }
     return pid;
   }
+
+  _getInstrumentationCrashError() {
+    return new DetoxRuntimeError({
+      message: 'Failed to run application on the device',
+      hint: this.instrumentationStackTrace
+        ? 'Most likely, your main activity has crashed prematurely.'
+        : 'Most likely, your tests have timed out and called detox.cleanup() ' +
+        'while it was waiting for "ready" message (over WebSocket) ' +
+        'from the instrumentation process.',
+      debugInfo: this.instrumentationStackTrace
+        ? `Native stacktrace dump: ${this.instrumentationStackTrace}`
+        : '',
+    });
+  }
 }
 
 module.exports = AndroidDriver;
diff --git a/detox/src/devices/drivers/android/tools/InstrumentationLogsParser.js b/detox/src/devices/drivers/android/InstrumentationLogsParser.js
similarity index 100%
rename from detox/src/devices/drivers/android/tools/InstrumentationLogsParser.js
rename to detox/src/devices/drivers/android/InstrumentationLogsParser.js
diff --git a/detox/src/devices/drivers/android/tools/InstrumentationLogsParser.test.js b/detox/src/devices/drivers/android/InstrumentationLogsParser.test.js
similarity index 100%
rename from detox/src/devices/drivers/android/tools/InstrumentationLogsParser.test.js
rename to detox/src/devices/drivers/android/InstrumentationLogsParser.test.js
diff --git a/detox/src/devices/drivers/android/tools/Instrumentation.js b/detox/src/devices/drivers/android/tools/Instrumentation.js
index b7f9f36aa..77714e707 100644
--- a/detox/src/devices/drivers/android/tools/Instrumentation.js
+++ b/detox/src/devices/drivers/android/tools/Instrumentation.js
@@ -34,6 +34,19 @@ class Instrumentation {
     return !!this.instrumentationProcess;
   }
 
+  setTerminationFn(userTerminationFn) {
+    this.userTerminationFn = userTerminationFn || _.noop;
+  }
+
+  setLogListenFn(userLogListenFn) {
+    this.userLogListenFn = userLogListenFn || _.noop;
+  }
+
+  clearAllCallbackFn() {
+    this.setTerminationFn(null);
+    this.setLogListenFn(null);
+  }
+
   _getSpawnArgs(userLaunchArgs) {
     const launchArgs = prepareInstrumentationArgs(userLaunchArgs);
     const additionalLaunchArgs = prepareInstrumentationArgs({ debug: false });
diff --git a/detox/src/devices/drivers/android/tools/MonitoredInstrumentation.js b/detox/src/devices/drivers/android/tools/MonitoredInstrumentation.js
deleted file mode 100644
index c88d4b1fe..000000000
--- a/detox/src/devices/drivers/android/tools/MonitoredInstrumentation.js
+++ /dev/null
@@ -1,82 +0,0 @@
-const _ = require('lodash');
-const Instrumentation = require('./Instrumentation');
-const { InstrumentationLogsParser } = require('./InstrumentationLogsParser');
-const Deferred = require('../../../../utils/Deferred');
-const DetoxRuntimeError = require('../../../../errors/DetoxRuntimeError');
-
-class MonitoredInstrumentation {
-  constructor(adb, logger) {
-    this.instrumentationLogsParser = null;
-    this.instrumentationStackTrace = '';
-    this.instrumentation = new Instrumentation(adb, logger, this._onInstrumentationTerminated.bind(this), this._onInstrumentationLogData.bind(this));
-    this.userTerminationFn = _.noop;
-    this.pendingPromise = Deferred.resolved();
-  }
-
-  async launch(deviceId, bundleId, userLaunchArgs) {
-    this.instrumentationLogsParser = new InstrumentationLogsParser();
-    await this.instrumentation.launch(deviceId, bundleId, userLaunchArgs);
-  }
-
-  setTerminationFn(userTerminationFn) {
-    this.userTerminationFn = userTerminationFn || _.noop;
-  }
-
-  waitForCrash() {
-    if (this.pendingPromise.isPending()) {
-      return this.pendingPromise.promise;
-    }
-
-    this.pendingPromise = new Deferred();
-    if (!this.instrumentation.isRunning()) {
-      this._rejectPendingCrashPromise();
-    }
-    return this.pendingPromise.promise;
-  }
-
-  abortWaitForCrash() {
-    this.pendingPromise.resolve();
-  }
-
-  isRunning() {
-    return this.instrumentation.isRunning();
-  }
-
-  async terminate() {
-    await this.instrumentation.terminate();
-    this._rejectPendingCrashPromise();
-  }
-
-  async _onInstrumentationTerminated() {
-    this._rejectPendingCrashPromise();
-    await this.userTerminationFn();
-  }
-
-  _rejectPendingCrashPromise() {
-    this.pendingPromise.reject(this._getInstrumentationCrashError());
-  }
-
-  _onInstrumentationLogData(logsDump) {
-    this.instrumentationLogsParser.parse(logsDump);
-
-    if (this.instrumentationLogsParser.containsStackTraceLog(logsDump)) {
-      this.instrumentationStackTrace = this.instrumentationLogsParser.getStackTrace(logsDump);
-    }
-  }
-
-  _getInstrumentationCrashError() {
-    return new DetoxRuntimeError({
-      message: 'Failed to run application on the device',
-      hint: this.instrumentationStackTrace
-        ? 'Most likely, your main activity has crashed prematurely.'
-        : 'Most likely, your tests have timed out and called detox.cleanup() ' +
-        'while it was waiting for "ready" message (over WebSocket) ' +
-        'from the instrumentation process.',
-      debugInfo: this.instrumentationStackTrace
-        ? `Native stacktrace dump: ${this.instrumentationStackTrace}`
-        : '',
-    });
-  }
-}
-
-module.exports = MonitoredInstrumentation;
diff --git a/detox/src/utils/Deferred.js b/detox/src/utils/Deferred.js
index 2a92c7a82..c9918d3a9 100644
--- a/detox/src/utils/Deferred.js
+++ b/detox/src/utils/Deferred.js
@@ -18,18 +18,6 @@ class Deferred {
     this.reject = this.reject.bind(this);
   }
 
-  static resolved(value) {
-    const deferred = new Deferred();
-    deferred.resolve(value);
-    return deferred;
-  }
-
-  static rejected(error) {
-    const deferred = new Deferred();
-    deferred.reject(error);
-    return deferred;
-  }
-
   get status() {
     return this[_status];
   }
@@ -38,18 +26,6 @@ class Deferred {
     return this[_promise];
   }
 
-  isPending() {
-    return this.status === Deferred.PENDING;
-  }
-
-  isResolved() {
-    return this.status === Deferred.RESOLVED;
-  }
-
-  isRejected() {
-    return this.status === Deferred.REJECTED;
-  }
-
   resolve(value) {
     if (this[_status] === Deferred.PENDING) {
       this[_resolve](value);
@@ -69,4 +45,4 @@ Deferred.PENDING = 'pending';
 Deferred.RESOLVED = 'resolved';
 Deferred.REJECTED = 'rejected';
 
-module.exports = Deferred;
+module.exports = Deferred;
\ No newline at end of file
diff --git a/docs/APIRef.DeviceObjectAPI.md b/docs/APIRef.DeviceObjectAPI.md
index d046def40..8a5d46018 100644
--- a/docs/APIRef.DeviceObjectAPI.md
+++ b/docs/APIRef.DeviceObjectAPI.md
@@ -29,7 +29,7 @@ The value will be `undefined` until the device is properly _prepared_ (i.e. in `
 - [`device.uninstallApp()`](#deviceuninstallapp)
 - [`device.openURL(url)`](#deviceopenurlurl-sourceappoptional)
 - [`device.sendUserNotification(params)`](#devicesendusernotificationparams)
-- [`device.sendUserActivity(params)` **iOS Only**](#devicesenduseractivityparams)
+- [`device.sendUserActivity(params)`](#devicesenduseractivityparams)
 - [`device.setOrientation(orientation)`](#devicesetorientationorientation)
 - [`device.setLocation(lat, lon)`](#devicesetlocationlat-lon)
 - [`device.setURLBlacklist([urls])`](#deviceseturlblacklisturls)
@@ -60,9 +60,7 @@ Launch the app defined in the current [`configuration`](APIRef.Configuration.md)
 
 ##### 1. `newInstance`—Launching a New Instance of the App
 
-Terminate the app and launch it again. 
-
-If set to `false`, the device will try to resume the app (e.g. bring from foreground to background). If the app isn't running, **it will launch a new instance** nonetheless. **Default is `false`.**
+Terminates the app and launch it again. If set to `false`, the simulator will try to bring app from background, if the app isn't running, it will launch a new instance. Default is `false`.
 
 ```js
 await device.launchApp({newInstance: true});
@@ -82,28 +80,29 @@ Detox uses [AppleSimUtils](https://github.com/wix/AppleSimulatorUtils) to implem
 Launches the app with the specified URL to test your app's deep link handling mechanism.
 
 ```js
-await device.launchApp({url});
-await device.launchApp({url, newInstance: true}); // Launch a new instance of the app
-await device.launchApp({url, newInstance: false}); // Reuse existing instance
+await device.launchApp({url: url});
+await device.launchApp({url: url, newInstance: true}); //Launch a new instance of the app
+await device.launchApp({url: url, newInstance: false}); //Reuse existing instance
 ```
+This will launch the app from background and handle the deep link.
 
-Read more [here](APIRef.MockingOpenWithURL.md). Go back to subsection 1 to read about the full effect of the `newInstance` argument.
+Read more in [here](APIRef.MockingOpenWithURL.md).
 
-##### 4. `userNotification`—Launching with User Notifications
+##### 4. `userNotification`—Launching with User Notifications (iOS Only)
 
-Launches with the specified user notification.
+Launches with the specified user notification
 
 ```js
-await device.launchApp({userNotification});
-await device.launchApp({userNotification, newInstance: true}); // Launch a new instance of the app
-await device.launchApp({userNotification, newInstance: false}); // Reuse existing instance
+await device.launchApp({userNotification: notification});
+await device.launchApp({userNotification: notification, newInstance: true}); //Launch a new instance of the app
+await device.launchApp({userNotification: notification, newInstance: false}); //Reuse existing instance
 ```
 
-Read more [here](APIRef.MockingUserNotifications.md). Go back to subsection 1 to read about the full effect of the `newInstance` argument.
+Read more in [here](APIRef.MockingUserNotifications.md).
 
 ##### 5. `userActivity`—Launch with User Activity (iOS Only)
 
-Launches the app with the specified user activity.
+Launches the app with the specified user activity
 
 ```js
 await device.launchApp({userActivity: activity});
@@ -111,7 +110,7 @@ await device.launchApp({userActivity: activity, newInstance: true}); //Launch a
 await device.launchApp({userActivity: activity, newInstance: false}); //Reuse existing instance
 ```
 
-Read more in [here](APIRef.MockingUserActivity.md). Go back to subsection 1 to read about the full effect of the `newInstance` argument.
+Read more in [here](APIRef.MockingUserActivity.md).
 
 ##### 6. `delete`—Delete and Reinstall Application Before Launching
 
@@ -212,7 +211,7 @@ await device.launchApp({
 
 ### `device.relaunchApp(params)`
 
-**Deprecated:** Use `device.launchApp(params)` instead. The method calls `launchApp({newInstance: true})` for backwards compatibility.
+**Deprecated:** Use `device.launchApp(params)` instead. This method is now calling `launchApp({newInstance: true})` for backwards compatibility.
 
 ### `device.terminateApp()`
 By default, `terminateApp()` with no params will terminate the app file defined in the current [`configuration`](APIRef.Configuration.md).
@@ -259,21 +258,16 @@ await device.uninstallApp('other.bundle.id');
 ```
 
 ### `device.openURL({url, sourceApp[optional]})`
-Mock opening the app from URL. `sourceApp` is an optional **iOS-only** parameter to specify source application bundle id.
-
+Mock opening the app from URL. `sourceApp` is an optional parameter to specify source application bundle id.
 Read more in [Mocking Open From URL](APIRef.MockingOpenFromURL.md) section.
 Check out Detox's [own test suite](../detox/test/e2e/15.urls.test.js)
 
 ### `device.sendUserNotification(params)`
-Mock handling of a user notification previously received in the system, while the app is already running.
-
+Mock handling of received user notification when app is in foreground.
 Read more in [Mocking User Notifications](APIRef.MockingUserNotifications.md) section.
 Check out Detox's [own test suite](../detox/test/e2e/11.user-notifications.test.js)
 
 ### `device.sendUserActivity(params)`
-
-> iOS-only API
-
 Mock handling of received user activity when app is in foreground.
 Read more in [Mocking User Activity](APIRef.MockingUserActivity.md) section.
 Check out Detox's [own test suite](../detox/test/e2e/18.user-activities.test.js)
diff --git a/docs/APIRef.MockingOpenWithURL.md b/docs/APIRef.MockingOpenWithURL.md
index 5e81cf71a..7a603f97f 100644
--- a/docs/APIRef.MockingOpenWithURL.md
+++ b/docs/APIRef.MockingOpenWithURL.md
@@ -5,19 +5,22 @@ You can mock opening the app from URL to test your app's deep link handling mech
 ## Mocking App Launch with a URL
 
 ```js
-await device.launchApp({newInstance: true, url, sourceApp: bundleId}); // sourceApp is an optional iOS-only argument
+await device.launchApp({newInstance: true, url: url, sourceApp: bundleId}); //sourceApp is optional
 ```
 
 #### Example
 
 ```js
 describe('launch app from URL', () => {
-    it('should handle URL successfully', async () => {
+    before(async () => {
       await device.launchApp({
         newInstance: true,
         url: 'scheme://some.url',
         sourceApp: 'com.apple.mobilesafari'
       });
+    });
+
+    it('should tap successfully', async () => {
       await expect(element(by.text('a label'))).toBeVisible();
     });
   });
@@ -25,8 +28,6 @@ describe('launch app from URL', () => {
 
 ## Mocking Opening with a URL on a Launched App
 
-> iOS-only
-
 ```js
 await device.openURL({url: 'scheme://some.url', sourceApp: 'com.apple.mobilesafari'});
 ```
diff --git a/docs/APIRef.MockingUserNotifications.md b/docs/APIRef.MockingUserNotifications.md
index 2f564c204..acff7e86a 100644
--- a/docs/APIRef.MockingUserNotifications.md
+++ b/docs/APIRef.MockingUserNotifications.md
@@ -1,12 +1,12 @@
 # Mocking User Notifications
 
-Detox supports mocking user notifications.
+Detox supports mocking user notifications for iOS apps.
 
->**Note:** The mocking mechanism will not mimic the UI of a user notification. Instead, it will only simulate a user interaction with the notification - namely, the "opening" of it (equivalent to a user's tap/swipe on it in the notification center).
+>**Note:** The mocking mechanism will not mimic the UI of a user notification. Instead, it will only simulate a user interaction with the notification.
 
 ## Mocking App Launch with a Notification
 
-`launchApp()` with custom parameters (i.e. `userNotification`) will trigger the mocking mechanism.
+Using `launchApp()` with custom params (e.g., `userNotification`) will trigger the mocking mechanism.
 
 ```js
 await device.launchApp({newInstance: true, userNotification: notification});
@@ -15,12 +15,15 @@ await device.launchApp({newInstance: true, userNotification: notification});
 #### Example
 
 ```js
-describe('Launch with push notification', () => {
-  it('should handle the notification', async () => {
+describe('Background push notification', () => {
+  beforeEach(async () => {
     await device.launchApp({
       newInstance: true,
       userNotification: userNotificationPushTrigger,
     });
+  });
+
+  it('push notification from background', async () => {
     await expect(element(by.text('From push'))).toBeVisible();
   });
 });
@@ -28,46 +31,49 @@ describe('Launch with push notification', () => {
 
 ## Mocking Notification Reception on a Running App
 
-Use the `sendUserNotification()` method to send notification to **running** app. Notifications can be sent to an active or a background app.
-
-> Note: while the name `sendUserNotification()` is not very idiodmatic on Android, as notification data is not "sent" to apps (rather, it is bundled into an Activity/Service launch Intent as Intent-extras), this unified API is used, for the time being, for both platforms equivalently. With [plans of a more extensive support](https://github.com/wix/Detox/issues/2141) for Android, we estimate it would be deprecated when the time comes.
+Use the `sendUserNotification()` method to send notification to running app. Notifications can be sent to an active or a background app.
 
 ```js
-await device.sendUserNotification(notification);
+await device.sendUserNotification(notification)
 ```
 
 **Example:**
 
 ```js
 describe('Foreground user notifications', () => {
-  it('should handle the local notification from inside the app', async () => {
-    await device.launchApp();
-    await device.sendUserNotification(localNotification);
-    await expect(element(by.text('from local notificaton'))).toBeVisible();
-   });
+
+beforeEach(async () => {
+  await device.launchApp();
+});
+
+it('Local notification from inside the app', async () => {
+  await device.sendUserNotification(localNotification);
+  await expect(element(by.text('from local notificaton'))).toBeVisible();
+ });
 });
 ```
 
 ## Notification JSON Format
 
-User notifications are passed as JSON objects to Detox. The JSON object needs to provide some required data, but can also provide an additional, optional payload.
 
-**Mind the major difference here between the two platforms.** While on iOS many types of data fields are applicable, Android is very loosely defined - with support for just free-form user data in the `payload` field.
+User notifications are passed as JSON objects to Detox. The JSON object needs to provide some required data, but can also provide additional, optional data.
 
-| Key | Required | Value Type | Platform | Description |
-|---------------------|----------|------------|---------------------------------------------------------------------------------------------------|---------------------|
-| `trigger` | Yes | Object | iOS | The conditions that trigger the delivery of the notification. See the [Triggers section](https://github.com/wix/detox/wiki/User-Notifications-JSON-Format-Documentation#triggers) below. |
-| `title` | No | String | iOS | A short description of the reason for the alert. |
-| `subtitle` | No | String | iOS | A secondary description of the reason for the alert. |
-| `body` | No | String | iOS | The body of the notification. |
-| `badge` | No | Integer | iOS | The number to display as the app’s icon badge. |
-| `payload` | iOS: No<br />Android: Yes | Object | iOS & Android | An object of custom information associated with the notification.<br />Android: see [full description below](#Payload) |
-| `category` | No | String | iOS | The identifier of the app-defined category object. |
-| `content-available` | No | Integer | iOS | Include this key with a value of 1 to configure a silent notification. |
-| `user-text` | No | String | iOS | The text response provided by the user. |
-| `action-identifier` | No | String | iOS | The identifier for the action that the user selected. |
+<!--- Use http://www.tablesgenerator.com/markdown_tables to edit these tables. --->
 
-### Triggers (iOS-only)
+| Key | Required | Value Type | Description |
+|---------------------|----------|------------|---------------------------------------------------------------------------------------------------|
+| `trigger` | Yes | Object | The conditions that trigger the delivery of the notification. See the [Triggers section](https://github.com/wix/detox/wiki/User-Notifications-JSON-Format-Documentation#triggers) below. |
+| `title` | No | String | A short description of the reason for the alert. |
+| `subtitle` | No | String | A secondary description of the reason for the alert. |
+| `body` | No | String | The body of the notification. |
+| `badge` | No | Integer | The number to display as the app’s icon badge. |
+| `payload` | No | Object | An object of custom information associated with the notification. |
+| `category` | No | String | The identifier of the app-defined category object. |
+| `content-available` | No | Integer | Include this key with a value of 1 to configure a silent notification. |
+| `user-text` | No | String | The text response provided by the user. |
+| `action-identifier` | No | String | The identifier for the action that the user selected. |
+
+### Triggers
 
 Triggers are objects representing the trigger.
 
@@ -100,7 +106,7 @@ const userNotification = {
 }
 ```
 
-### Date Components (iOS-only)
+### Date Components
 
 | Key | Required | Value Type | Description |
 |------------------|----------|------------|-------------------------------------------------------|
@@ -117,7 +123,7 @@ const userNotification = {
 | `weekOfMonth` | No | Integer | The week number of the month for the receiver. |
 | `leapMonth` | No | Boolean | Indicates whether the month is a leap month. |
 
-### Region (iOS-only)
+### Region
 
 | Key | Required | Value Type | Description |
 |-----------------|----------|------------|------------------------------------------------------------------------------------|
@@ -126,48 +132,13 @@ const userNotification = {
 | `notifyOnEntry` | No | Boolean | Indicates that notifications are generated upon entry into the region. |
 | `notifyOnExit` | No | Boolean | Indicates that notifications are generated upon exit from the region. |
 
-### Coordinate (iOS-only)
+### Coordinate
 
 | Key | Required | Value Type | Description |
 |-------------|----------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | `latitude` | Yes | Number | The latitude in degrees. Positive values indicate latitudes north of the equator. Negative values indicate latitudes south of the equator. |
 | `longitude` | Yes | Number | The longitude in degrees. Measurements are relative to the zero meridian, with positive values extending east of the meridian and negative values extending west of the meridian. |
 
-### Payload
-
-On Android, the content will be available via the activity's [`getIntent()`](https://developer.android.com/reference/android/app/Activity#getIntent()) API, inside the intent's _extra_ bundle. Under some limitations, that includes data-cascading so as to provide comprehensive support for Javascript's advanced object-hierarchy capabilities as much as possible. As an example, consider this payload:
-
-```javascript
-  const userNotification = {
-    payload: {
-      userData: 'userDataValue',
-      userDataNum: 111.2,
-      userDataFlag: true,
-      userDataArray: ['rock', 'paper', 'scissors'],
-      userDataObj: {
-        cascadedKey: 'cascadedValue'
-      },
-    },
-  };
-
-```
-
-The outcome on the native side will be such that all of these conditions evaluate to _true_:
-
-```java
-activity.getIntent().getStringExtra("userData") == "userDataValue";
-activity.getIntent().getDoubleExtra("userDataNum") == 111.2;
-activity.getIntent().getBooleanExtra("userDataFlag") == true;
-activity.getIntent().getStringArrayExtra("userDataArray")[0] == "rock";
-activity.getIntent().getBundleExtra("userDataObj").getString("cascadedKey") == "cascadedValue";
-```
-
-#### Handling at Runtime
-
-Note that on Android, data delivered through an intent at runtime, is typically received in your activity's [`onNewIntent`](https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent)) callback. Be sure to consider what should be done in order to handle this type of a use case in your app: Namely, that `setIntent()` should be called in order for the data to be later available in your app through `getIntent()`, as explained earlier.
-
->  **This isn't related to Detox in particular**, and is set here simply to help you consider all the use cases in your app so that tests coverage would be optimal.
-
 ### Examples
 
 1. [Calendar Trigger](https://github.com/wix/detox/blob/master/detox/ios/DetoxUserNotificationTests/user_notification_calendar_trigger.json)


Test patch:
diff --git a/detox/src/devices/drivers/android/AndroidDriver.test.js b/detox/src/devices/drivers/android/AndroidDriver.test.js
index 6017fc15d..67466d6b1 100644
--- a/detox/src/devices/drivers/android/AndroidDriver.test.js
+++ b/detox/src/devices/drivers/android/AndroidDriver.test.js
@@ -2,7 +2,6 @@ describe('Android driver', () => {
 
   const deviceId = 'device-id-mock';
   const bundleId = 'bundle-id-mock';
-  const detoxServerPort = 1234;
   const mockNotificationDataTargetPath = '/ondevice/path/to/notification.json';
 
   const mockGetAbsoluteBinaryPathImpl = (x) => `absolutePathOf(${x})`;
@@ -23,13 +22,28 @@ describe('Android driver', () => {
     }
   }
 
+  class MockAsyncEmitterClass {
+    constructor() {
+      this.emit = jest.fn();
+    }
+  }
+
+  class MockInstrumentationLogsParserClass {
+    constructor() {
+      this.parse = () => {};
+      this.containsStackTraceLog = () => true;
+      this.getStackTrace = () => MockInstrumentationLogsParserClass.INSTRUMENTATION_STACKTRACE_MOCK;
+    }
+  }
+  MockInstrumentationLogsParserClass.INSTRUMENTATION_STACKTRACE_MOCK = 'Stacktrace mock';
+
   class MockInvocationManagerClass {
     constructor() {
       Object.assign(this, invocationManager);
     }
   }
 
-  class MockMonitoredInstrumentationClass {
+  class MockInstrumentationClass {
     constructor(...args) {
       instrumentation.mockCtor(...args);
       Object.assign(this, instrumentation);
@@ -41,8 +55,8 @@ describe('Android driver', () => {
   let getAbsoluteBinaryPath;
   let fs;
   let exec;
-  let emitter;
   let detoxApi;
+  let instrumentationArgs;
   let instrumentation;
   beforeEach(() => {
     jest.mock('fs', () => ({
@@ -58,6 +72,10 @@ describe('Android driver', () => {
     jest.mock('../../../utils/sleep', () => jest.fn().mockResolvedValue(''));
     jest.mock('../../../utils/retry', () => jest.fn().mockResolvedValue(''));
 
+    jest.mock('./InstrumentationLogsParser', () => ({
+      InstrumentationLogsParser: MockInstrumentationLogsParserClass,
+    }));
+
     jest.mock('../../../utils/getAbsoluteBinaryPath', () =>
       jest.fn().mockImplementation((x) => `absolutePathOf(${x})`),
     );
@@ -83,24 +101,23 @@ describe('Android driver', () => {
 
     client = {
       configuration: {
-        server: `ws://localhost:${detoxServerPort}`
+        server: 'ws://localhost:1234'
       },
       waitUntilReady: jest.fn(),
     };
 
-    emitter = {
-      emit: jest.fn(),
-      off: jest.fn(),
-    };
-
     jest.mock('../../../android/espressoapi/Detox');
     detoxApi = require('../../../android/espressoapi/Detox');
 
-    const MonitoredInstrumentation = jest.genMockFromModule('./tools/MonitoredInstrumentation');
-    instrumentation = new MonitoredInstrumentation();
+    jest.mock('./tools/instrumentationArgs');
+    instrumentationArgs = require('./tools/instrumentationArgs');
+    instrumentationArgs.prepareInstrumentationArgs.mockReturnValue({args: [], usedReservedArgs: []});
+
+    const Instrumentation = jest.genMockFromModule('./tools/Instrumentation');
+    instrumentation = new Instrumentation();
     instrumentation.mockCtor = jest.fn();
     mockInstrumentationDead();
-    jest.mock('./tools/MonitoredInstrumentation', () => MockMonitoredInstrumentationClass);
+    jest.mock('./tools/Instrumentation', () => MockInstrumentationClass);
   });
 
   let adb;
@@ -142,7 +159,7 @@ describe('Android driver', () => {
     const AndroidDriver = require('./AndroidDriver');
     uut = new AndroidDriver({
       client,
-      emitter,
+      emitter: new MockAsyncEmitterClass(),
     });
   });
 
@@ -167,64 +184,6 @@ describe('Android driver', () => {
         fail();
       } catch (e) {}
     });
-
-    it('should set a termination callback function', async () => {
-      await uut.launchApp(deviceId, bundleId, {}, '');
-      expect(instrumentation.setTerminationFn).toHaveBeenCalledWith(expect.any(Function));
-    });
-
-    it('should adb-reverse the detox server port', async () => {
-      await uut.launchApp(deviceId, bundleId, {}, '');
-      await expect(adb.reverse).toHaveBeenCalledWith(deviceId, detoxServerPort.toString());
-    });
-  });
-
-  describe('Instrumentation unexpected termination', () => {
-    beforeEach(async () => {
-      await uut.launchApp(deviceId, bundleId, {}, '');
-      await invokeTerminationCallbackFn();
-    });
-
-    it('should clear out the termination callback function', () =>
-      expect(instrumentation.setTerminationFn).toHaveBeenCalledWith(null));
-
-    it('should adb-unreverse the detox server port', () =>
-      expect(adb.reverseRemove).toHaveBeenCalledWith(deviceId, detoxServerPort.toString()));
-
-    const extractTerminationCallbackFn = () => instrumentation.setTerminationFn.mock.calls[0][0];
-    const invokeTerminationCallbackFn = async () => {
-      const fn = extractTerminationCallbackFn();
-      await fn();
-    }
-  });
-
-  describe('App termination', () => {
-    beforeEach(async () => {
-      await uut.launchApp(deviceId, bundleId, {}, '');
-      await uut.terminate();
-    });
-
-    it('should terminate instrumentation', () =>
-      expect(instrumentation.terminate).toHaveBeenCalled());
-
-    it('should clear out the termination callback function', () =>
-      expect(instrumentation.setTerminationFn).toHaveBeenCalledWith(null));
-
-    it('should terminate ADB altogether', () =>
-      expect(adb.terminate).toHaveBeenCalled());
-  });
-
-  describe('Cleanup', () => {
-    beforeEach(async () => await uut.cleanup());
-
-    it('should terminate instrumentation', () =>
-      expect(instrumentation.terminate).toHaveBeenCalled());
-
-    it('should clear out the termination callback function', () =>
-      expect(instrumentation.setTerminationFn).toHaveBeenCalledWith(null));
-
-    it('should turn off the events emitter', () =>
-      expect(emitter.off).toHaveBeenCalled());
   });
 
   describe('URL runtime delivery handling', () => {
@@ -403,59 +362,45 @@ describe('Android driver', () => {
 
   describe('Device ready-wait', () => {
     it('should delegate wait to device being ready via client api', async () => {
+      mockInstrumentationRunning();
       await uut.waitUntilReady();
       expect(client.waitUntilReady).toHaveBeenCalled();
     }, 2000);
 
     it('should fail if instrumentation async\'ly-dies prematurely while waiting for device-ready resolution', async () => {
-      const crashError = new Error('mock instrumentation crash error');
-      let waitForCrashReject = () => {};
-      instrumentation.waitForCrash.mockImplementation(() => {
-        return new Promise((__, reject) => {
-          waitForCrashReject = reject;
-        });
-      });
-
       await uut.launchApp(deviceId, bundleId, {}, '');
 
+      mockInstrumentationRunning();
       const clientWaitResolve = mockDeviceReadyPromise();
+
       const promise = uut.waitUntilReady();
-      setTimeout(() => waitForCrashReject(crashError), 1);
+      setTimeout(fakeInstrumentationTermination, 1);
 
       try {
         await promise;
         fail('Expected an error and none was thrown');
       } catch (e) {
-        expect(e).toEqual(crashError);
+        expect(e.toString()).toContain('DetoxRuntimeError: Failed to run application on the device');
+        expect(e.toString()).toContain(`Native stacktrace dump: ${MockInstrumentationLogsParserClass.INSTRUMENTATION_STACKTRACE_MOCK}`);
       } finally {
         clientWaitResolve();
       }
     }, 2000);
 
-    it('should abort crash-wait if instrumentation doesnt crash', async () => {
-      client.waitUntilReady.mockResolvedValue('mocked');
-      await uut.waitUntilReady();
-      expect(instrumentation.abortWaitForCrash).toHaveBeenCalled();
-    });
-
-    it('should abort crash-wait if instrumentation crashes', async () => {
-      client.waitUntilReady.mockResolvedValue('mocked');
-      instrumentation.waitForCrash.mockRejectedValue(new Error());
-
-      await uut.launchApp(deviceId, bundleId, {}, '');
-      try {
-        await uut.waitUntilReady();
-        fail();
-      } catch (e) {
-        expect(instrumentation.abortWaitForCrash).toHaveBeenCalled();
-      }
-    });
-
     const mockDeviceReadyPromise = () => {
       let clientResolve;
       client.waitUntilReady.mockReturnValue(new Promise((resolve) => clientResolve = resolve));
       return clientResolve;
     };
+
+    const fakeInstrumentationTermination = async () => {
+      const instrumentationLoggingCallback = instrumentation.setLogListenFn.mock.calls[0][0];
+      await instrumentationLoggingCallback('Doesnt matter what we put here because actual stacktrace should be read from logs-parser');
+
+      const instrumentationTerminationCallback = instrumentation.setTerminationFn.mock.calls[0][0];
+      await instrumentationTerminationCallback();
+      mockInstrumentationDead();
+    };
   });
 
   describe('App installation', () => {
diff --git a/detox/src/devices/drivers/android/tools/Instrumentation.test.js b/detox/src/devices/drivers/android/tools/Instrumentation.test.js
index 31089ed68..c6b4a1233 100644
--- a/detox/src/devices/drivers/android/tools/Instrumentation.test.js
+++ b/detox/src/devices/drivers/android/tools/Instrumentation.test.js
@@ -211,11 +211,53 @@ describe('Instrumentation', () => {
     });
   });
 
-  it('should work with no user callbacks registration', async () => {
+  it('should allow for runtime setup of termination callback', async () => {
+    const runtimeCallback = jest.fn();
+
     const Instrumentation = require('./Instrumentation');
     uut = new Instrumentation(adb, logger, undefined, undefined);
+    uut.setTerminationFn(runtimeCallback);
+    await uut.launch(deviceId, bundleId, []);
+    await invokeTerminationCallback();
+
+    expect(runtimeCallback).toHaveBeenCalled();
+  });
+
+  it('should allow for clearing of termination callback', async () => {
+    uut.setTerminationFn(null);
     await uut.launch(deviceId, bundleId, []);
     await uut.terminate();
+    expect(userTerminationCallback).not.toHaveBeenCalled();
+  });
+
+  it('should allow for runtime setup of log-tapping callback', async () => {
+    const runtimeCallback = jest.fn();
+
+    const Instrumentation = require('./Instrumentation');
+    uut = new Instrumentation(adb, logger, undefined, undefined);
+    await uut.launch(deviceId, bundleId, []);
+
+    uut.setLogListenFn(runtimeCallback);
+    await invokeDataCallbackWith('mock data');
+
+    expect(runtimeCallback).toHaveBeenCalledWith('mock data');
+  });
+
+  it('should allow for clearing of log-tapping callback', async () => {
+    uut.setLogListenFn(null);
+    await uut.launch(deviceId, bundleId, []);
+    await invokeDataCallbackWith('data');
+    expect(logListenCallback).not.toHaveBeenCalled();
+  });
+
+  it('should allow for clearing of all user-callbacks', async () => {
+    uut.clearAllCallbackFn();
+
+    await uut.launch(deviceId, bundleId, []);
+    await invokeDataCallbackWith('data');
+    await invokeTerminationCallback();
+    expect(userTerminationCallback).not.toHaveBeenCalled();
+    expect(logListenCallback).not.toHaveBeenCalled();
   });
 
   const extractDataCallback = () => childProcess.stdout.on.mock.calls[0][1];
diff --git a/detox/src/devices/drivers/android/tools/MonitoredInstrumentation.test.js b/detox/src/devices/drivers/android/tools/MonitoredInstrumentation.test.js
deleted file mode 100644
index d0393ce0c..000000000
--- a/detox/src/devices/drivers/android/tools/MonitoredInstrumentation.test.js
+++ /dev/null
@@ -1,231 +0,0 @@
-describe('Monitored instrumentation', () => {
-  const deviceId = 'mock-device-id';
-  const bundleId = 'mock-bundle-id';
-
-  class MockInstrumentationClass {
-    constructor(...args) {
-      instrumentation.mockCtor(...args);
-      Object.assign(this, instrumentation);
-    }
-  }
-
-  class MockInstrumentationLogsParserClass {
-    constructor() {
-      Object.assign(this, instrumentationLogsParser);
-    }
-  }
-  MockInstrumentationLogsParserClass.INSTRUMENTATION_STACKTRACE_MOCK = 'Stacktrace mock';
-
-  let adb;
-  let logger;
-  beforeEach(() => {
-    const ADB = jest.genMockFromModule('../exec/ADB');
-    adb = new ADB();
-
-    logger = {};
-  });
-
-  let instrumentation;
-  let instrumentationLogsParser;
-  beforeEach(() => {
-
-    const Instrumentation = jest.genMockFromModule('./Instrumentation');
-    instrumentation = new Instrumentation();
-    instrumentation.mockCtor = jest.fn();
-    jest.mock('./Instrumentation', () => MockInstrumentationClass);
-
-    const { InstrumentationLogsParser } = jest.genMockFromModule('./InstrumentationLogsParser');
-    instrumentationLogsParser = new InstrumentationLogsParser();
-    instrumentationLogsParser.getStackTrace.mockReturnValue(MockInstrumentationLogsParserClass.INSTRUMENTATION_STACKTRACE_MOCK);
-    jest.mock('./InstrumentationLogsParser', () => ({
-      InstrumentationLogsParser: MockInstrumentationLogsParserClass,
-    }));
-  });
-
-  let uut;
-  beforeEach(() => {
-    const MonitoredInstrumentation = require('./MonitoredInstrumentation');
-    uut = new MonitoredInstrumentation(adb, logger);
-  });
-
-  it('should properly init the underlying instrumentation', () => {
-    expect(instrumentation.mockCtor).toHaveBeenCalledWith(adb, logger, expect.any(Function), expect.any(Function));
-  });
-
-  describe('launch', () => {
-    it('should launch the underlying instrumentation', async () => {
-      const launchArgs = {};
-      await uut.launch(deviceId, bundleId, launchArgs);
-      expect(instrumentation.launch).toHaveBeenCalledWith(deviceId, bundleId, launchArgs);
-    });
-
-    it('should break if underlying launch fails', async () => {
-      instrumentation.launch.mockRejectedValue(new Error());
-
-      try {
-        await uut.launch(deviceId, bundleId, {});
-        fail();
-      } catch (e) {}
-    });
-  });
-
-  describe('Unexpected termination', () => {
-    it('should invoke user termination callback', async () => {
-      const terminationFn = jest.fn();
-      uut.setTerminationFn(terminationFn);
-      await uut.launch(deviceId, bundleId, {});
-      await invokeUnderlyingTerminationCallback();
-      expect(terminationFn).toHaveBeenCalled();
-    });
-  });
-
-  describe('Initiation termination', () => {
-    it('should terminate the underlying instrumentation', async () => {
-      await uut.launch(deviceId, bundleId, {});
-      await uut.terminate();
-      expect(instrumentation.terminate).toHaveBeenCalled();
-    });
-
-    it('should break if underlying termination fails', async () => {
-      instrumentation.terminate.mockRejectedValue(new Error());
-
-      await uut.launch(deviceId, bundleId, {});
-
-      try {
-        await uut.terminate();
-        fail();
-      } catch (e) {}
-    });
-
-    it('should allow for termination without launch', async () => {
-      await uut.terminate();
-      expect(instrumentation.terminate).toHaveBeenCalled();
-    });
-  });
-
-  it('should allow for user-initiated clearing of termination callback function', async () => {
-    const terminationFn = jest.fn();
-    uut.setTerminationFn(terminationFn);
-    await uut.launch(deviceId, bundleId, {});
-
-    uut.setTerminationFn(null);
-    await invokeUnderlyingTerminationCallback();
-    expect(terminationFn).not.toHaveBeenCalled();
-  });
-
-  it('should query underlying instrumentation for status', async () => {
-    mockUnderlyingInstrumentationRunning();
-    expect(uut.isRunning()).toEqual(true);
-
-    mockUnderlyingInstrumentationDead();
-    expect(uut.isRunning()).toEqual(false);
-  });
-
-  describe('Crash monitoring', () => {
-    let onReject;
-    beforeEach(() => {
-      onReject = jest.fn();
-
-      mockUnderlyingInstrumentationRunning();
-    });
-
-    it('should signal termination due to unexpected underlying termination, if waited-for', async () => {
-      await uut.launch(deviceId, bundleId, {});
-      uut.waitForCrash().catch(onReject);
-
-      await invokeUnderlyingTerminationCallback();
-      expect(onReject).toHaveBeenCalled();
-    });
-
-    it('should signal termination due to initiated termination, if waited-for', async () => {
-      await uut.launch(deviceId, bundleId, {});
-      uut.waitForCrash().catch(onReject);
-
-      await uut.terminate();
-      expect(onReject).toHaveBeenCalled();
-    });
-
-    it('should signal termination with a parsed stack-trace', async () => {
-      instrumentationLogsParser.containsStackTraceLog.mockReturnValue(true);
-
-      await uut.launch(deviceId, bundleId, {});
-      uut.waitForCrash().catch(onReject);
-
-      await invokeUnderlyingLogListenerCallbackWith('mock data');
-      await invokeUnderlyingTerminationCallback();
-
-      assertRejectedWithNativeStacktrace();
-    });
-
-    it('should signal termination with no stack-trace, if none available', async () => {
-      instrumentationLogsParser.containsStackTraceLog.mockReturnValue(false);
-
-      await uut.launch(deviceId, bundleId, {});
-      uut.waitForCrash().catch(onReject);
-
-      await invokeUnderlyingLogListenerCallbackWith('mock data');
-      await invokeUnderlyingTerminationCallback();
-
-      expect(instrumentationLogsParser.parse).toHaveBeenCalledWith('mock data');
-
-      assertRejectedWithoutStacktrace();
-    });
-
-    it('should allow for user-initiated clearing of app-wait', async () => {
-      const onResolve = jest.fn();
-
-      await uut.launch(deviceId, bundleId, {});
-      const promise = uut.waitForCrash().then(onResolve);
-      uut.abortWaitForCrash();
-
-      await promise;
-      expect(onResolve).toHaveBeenCalled();
-    });
-
-    it('should immediately signal termination if already terminated', async () => {
-      await uut.launch(deviceId, bundleId, {});
-
-      mockUnderlyingInstrumentationDead();
-      await uut.waitForCrash().catch(onReject);
-
-      expect(onReject).toHaveBeenCalled();
-    });
-
-    it('should account for all waiting crash-wait clients', async () => {
-      await uut.launch(deviceId, bundleId, {});
-
-      uut.waitForCrash().catch(onReject);
-      uut.waitForCrash().catch(onReject);
-
-      await uut.terminate();
-
-      expect(onReject).toHaveBeenCalledTimes(2);
-    });
-
-    const assertRejectedWithNativeStacktrace = () => {
-      const e = onReject.mock.calls[0][0];
-      expect(e.toString()).toContain('DetoxRuntimeError: Failed to run application on the device');
-      expect(e.toString()).toContain(`Native stacktrace dump: ${MockInstrumentationLogsParserClass.INSTRUMENTATION_STACKTRACE_MOCK}`);
-    };
-
-    const assertRejectedWithoutStacktrace = () => {
-      const e = onReject.mock.calls[0][0];
-      expect(e.toString()).toContain('DetoxRuntimeError: Failed to run application on the device');
-      expect(e.toString()).not.toContain('Native stacktrace dump');
-    };
-  });
-
-  const extractUnderlyingTerminationCallback = () => instrumentation.mockCtor.mock.calls[0][2];
-  const invokeUnderlyingTerminationCallback = async () => {
-    const fn = extractUnderlyingTerminationCallback();
-    await fn();
-  }
-  const extractUnderlyingLogListenerCallback = () => instrumentation.mockCtor.mock.calls[0][3];
-  const invokeUnderlyingLogListenerCallbackWith = async (data) => {
-    const fn = extractUnderlyingLogListenerCallback();
-    await fn(data);
-  }
-
-  const mockUnderlyingInstrumentationRunning = () => instrumentation.isRunning.mockReturnValue(true);
-  const mockUnderlyingInstrumentationDead = () => instrumentation.isRunning.mockReturnValue(false);
-});
diff --git a/detox/src/utils/Deferred.test.js b/detox/src/utils/Deferred.test.js
index e6bf3ac1a..52240cdfd 100644
--- a/detox/src/utils/Deferred.test.js
+++ b/detox/src/utils/Deferred.test.js
@@ -23,12 +23,6 @@ describe('Deferred', () => {
     it('should have pending status', () =>
       expect(deferred.status).toBe(Deferred.PENDING));
 
-    it('should indicate an isPending=true, other statuses as false', () => {
-      expect(deferred.isPending()).toEqual(true);
-      expect(deferred.isResolved()).toEqual(false);
-      expect(deferred.isRejected()).toEqual(false);
-    });
-
     it('should have promise', () =>
       expect(deferred.promise).toBeInstanceOf(Promise));
 
@@ -44,12 +38,6 @@ describe('Deferred', () => {
       it('should have a resolved status', () =>
         expect(deferred.status).toBe(Deferred.RESOLVED));
 
-      it('should indicate an isResolved=true, other statuses as false', () => {
-        expect(deferred.isResolved()).toEqual(true);
-        expect(deferred.isPending()).toEqual(false);
-        expect(deferred.isRejected()).toEqual(false);
-      });
-
       it('should have a resolved promise', () =>
         expect(deferred.promise).resolves.toBe(42));
 
@@ -73,12 +61,6 @@ describe('Deferred', () => {
       it('should have a rejected promise', () =>
         expect(deferred.promise).rejects.toThrowError('RejectionTest'));
 
-      it('should indicate an isRejected=true, other statuses as false', () => {
-        expect(deferred.isRejected()).toEqual(true);
-        expect(deferred.isResolved()).toEqual(false);
-        expect(deferred.isPending()).toEqual(false);
-      });
-
       describe('and then resolved', () => {
         beforeEach(() => deferred.resolve(42));
 
@@ -90,27 +72,4 @@ describe('Deferred', () => {
       });
     });
   });
-
-  describe('when precreated as resolved', () => {
-    it('should be resolved', async () => {
-      deferred = Deferred.resolved('mock resolution');
-
-      expect(deferred.status).toBe(Deferred.RESOLVED);
-      expect(await deferred.promise).toEqual('mock resolution');
-    });
-  });
-
-  describe('when precreated as rejected', () => {
-    it('should be resolved', async () => {
-      deferred = Deferred.rejected(new Error('error mock'));
-
-      expect(deferred.status).toBe(Deferred.REJECTED);
-      try {
-        await deferred.promise;
-        fail();
-      } catch (e) {
-        expect(e.message).toEqual('error mock');
-      }
-    });
-  });
 });