package com.mindbodyonline.ironhide.Infrastructure.Extensions;
import android.annotation.TargetApi;
import android.os.SystemClock;
import android.support.test.espresso.InjectEventSecurityException;
import android.support.test.espresso.PerformException;
import android.support.test.espresso.UiController;
import android.util.Log;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
import android.view.MotionEvent.PointerProperties;
import android.view.ViewConfiguration;
import com.android.support.test.deps.guava.annotations.VisibleForTesting;
import java.util.Arrays;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_POINTER_DOWN;
import static android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT;
import static android.view.MotionEvent.ACTION_POINTER_UP;
import static android.view.MotionEvent.ACTION_UP;
import static android.view.MotionEvent.TOOL_TYPE_FINGER;
import static com.android.support.test.deps.guava.base.Preconditions.checkNotNull;
/**
* Facilitates sending of motion events to a {@link UiController}.
*/
@TargetApi(14)
final class ZoomMotionEvents {
@VisibleForTesting
static final int MAX_CLICK_ATTEMPTS = 3;
public static final int MIN_LOOP_TIME = 10; /* milliseconds */
// default values for MotionEvent.obtain
private static final int DEFAULT_BUTTON_STATE = 0;
private static final int DEFAULT_DEVICE_ID = 0;
private static final int DEFAULT_EDGE_FLAGS = 0;
private static final int DEFAULT_FLAGS = 0;
private static final int DEFAULT_META_STATE = 0;
private static final int DEFAULT_PRESSURE = 1;
private static final int DEFAULT_SIZE = 1;
private static final int DEFAULT_SOURCE = 0;
private static final String TAG = ZoomMotionEvents.class.getSimpleName();
private static final PointerProperties[] defaultProperties;
static {
defaultProperties = new PointerProperties[]{ new PointerProperties(), new PointerProperties() };
defaultProperties[0].id = 0;
defaultProperties[1].id = 1;
defaultProperties[0].toolType = defaultProperties[1].toolType = TOOL_TYPE_FINGER;
}
private UiController uiController;
private float[] precision;
public long downTime;
private MotionEvent[] downEvents;
public ZoomMotionEvents(UiController uiController, float[] precision) {
checkNotNull(uiController);
checkNotNull(precision);
this.uiController = uiController;
this.precision = precision;
}
public boolean sendDownPair(float[][] coordinates) {
checkNotNull(coordinates);
for (int retry = 0; retry < MAX_CLICK_ATTEMPTS; retry++) {
try {
downTime = SystemClock.uptimeMillis();
downEvents = new MotionEvent[] {
obtainWrapper(ACTION_DOWN, coordinates[0][0], coordinates[0][1], precision),
obtainWrapper(ACTION_POINTER_DOWN | 1 << ACTION_POINTER_INDEX_SHIFT, coordinates, precision)
};
long isTapAt = downTime + (ViewConfiguration.getTapTimeout() / 2);
boolean injectEventsSucceeded = uiController.injectMotionEvent(downEvents[0])
&& uiController.injectMotionEvent(downEvents[1]);
while (isTapAt - SystemClock.uptimeMillis() > MIN_LOOP_TIME) {
// Sleep only a fraction of the time, since there may be other events in the UI queue
// that could cause us to start sleeping late, and then oversleep.
uiController.loopMainThreadForAtLeast( (isTapAt - SystemClock.uptimeMillis()) / 4);
}
if (injectEventsSucceeded)
return true;
downEvents[0].recycle();
downEvents[1].recycle();
downEvents[0] = null;
downEvents[1] = null;
} catch (InjectEventSecurityException e) {
Log.e(TAG, Log.getStackTraceString(getPerformException("Send down motion events", e)));
}
}
Log.e(TAG, Log.getStackTraceString(getPerformException(String.format("click (after %d attempts)", MAX_CLICK_ATTEMPTS))));
return false;
}
public boolean sendMovementPair(float[][] coordinates) {
checkNotNull(coordinates);
MotionEvent motionEvent = null;
try {
motionEvent = obtainWrapper(ACTION_MOVE, coordinates, precision);
boolean injectEventSucceeded = uiController.injectMotionEvent(motionEvent);
if (!injectEventSucceeded) {
Log.e(TAG, String.format("Injection of motion event failed (corresponding down events: %s and %s)",
downEvents[0].toString(), downEvents[1].toString()));
return false;
}
} catch (InjectEventSecurityException e) {
Log.e(TAG, String.format("Injection of motion event failed (corresponding down events: %s and %s)",
downEvents[0].toString(), downEvents[1].toString()));
return false;
} finally {
if (motionEvent != null)
motionEvent.recycle();
}
return true;
}
public void sendCancelPair(float[][] coordinates) {
checkNotNull(coordinates);
MotionEvent motionEvent = null;
try {
// Up press.
motionEvent = obtainWrapper(ACTION_MOVE, coordinates, precision);
boolean injectEventSucceeded = uiController.injectMotionEvent(motionEvent);
if (!injectEventSucceeded) {
String description = String.format("inject cancel event (corresponding down event: %s and %s)", downEvents[0].toString(), downEvents[1].toString());
throw getPerformException(description);
}
} catch (InjectEventSecurityException e) {
String description = String.format("inject cancel event (corresponding down event: %s and %s)", downEvents[0].toString(), downEvents[1].toString());
throw getPerformException(description, e);
} finally {
if (motionEvent != null)
motionEvent.recycle();
}
}
public boolean sendUpPair(float[][] coordinates) {
checkNotNull(coordinates);
MotionEvent[] motionEvents = null;
try {
// Up press.
motionEvents = new MotionEvent[] {
obtainWrapper(ACTION_POINTER_UP | 1 << ACTION_POINTER_INDEX_SHIFT, coordinates, precision),
obtainWrapper(ACTION_UP, coordinates[0][0], coordinates[0][1], precision)
};
if ( !(uiController.injectMotionEvent(motionEvents[0]) && uiController.injectMotionEvent(motionEvents[1])) ) {
Log.e(TAG, String.format("Injection of up event failed (corresponding down events: %s)", Arrays.deepToString(downEvents)));
return false;
}
} catch (InjectEventSecurityException e) {
String description = String.format("inject up event (corresponding down events: %s and %s)", downEvents[0].toString(), downEvents[1].toString());
throw getPerformException(description, e);
} finally {
if (motionEvents != null) {
motionEvents[0].recycle();
motionEvents[1].recycle();
motionEvents[0] = null;
motionEvents[1] = null;
}
}
return true;
}
/**
* Helper functions
*/
private static PointerCoords[] getCoordinates(float[][] coordinates) {
PointerCoords[] pointerCoordinates = new PointerCoords[]{ new PointerCoords(), new PointerCoords() };
pointerCoordinates[0].pressure = pointerCoordinates[1].pressure = 1;
pointerCoordinates[0].size = pointerCoordinates[1].size = 1;
pointerCoordinates[0].x = coordinates[0][0];
pointerCoordinates[0].y = coordinates[0][1];
pointerCoordinates[1].x = coordinates[1][0];
pointerCoordinates[1].y = coordinates[1][1];
return pointerCoordinates;
}
private static PerformException getPerformException(String description, Throwable cause) {
return new PerformException.Builder()
.withActionDescription(description)
.withViewDescription("unknown") // likely to be replaced by FailureHandler
.withCause(cause)
.build();
}
private static PerformException getPerformException(String description) {
return new PerformException.Builder()
.withActionDescription(description)
.withViewDescription("unknown") // likely to be replaced by FailureHandler
.build();
}
private MotionEvent obtainWrapper(int action, float x, float y, float[] precision) {
return MotionEvent.obtain(
downTime, SystemClock.uptimeMillis(),
action,
x, y, DEFAULT_PRESSURE, DEFAULT_SIZE,
DEFAULT_META_STATE,
precision[0], precision[1],
DEFAULT_DEVICE_ID, DEFAULT_EDGE_FLAGS);
}
// 2D array implies user wishes to pass 2 pointer values
private MotionEvent obtainWrapper(int action, float[][] coordinates, float[] precision) {
return MotionEvent.obtain(
downTime, SystemClock.uptimeMillis(),
action,
2, defaultProperties, getCoordinates(coordinates),
DEFAULT_META_STATE, DEFAULT_BUTTON_STATE,
precision[0], precision[1],
DEFAULT_DEVICE_ID, DEFAULT_EDGE_FLAGS, DEFAULT_SOURCE, DEFAULT_FLAGS);
}
}