| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.google.android.apps.common.testing.ui.espresso.action; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| |
| import com.google.android.apps.common.testing.testrunner.UsageTrackerRegistry; |
| import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException; |
| import com.google.android.apps.common.testing.ui.espresso.PerformException; |
| import com.google.android.apps.common.testing.ui.espresso.UiController; |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import android.os.SystemClock; |
| import android.util.Log; |
| import android.view.MotionEvent; |
| import android.view.ViewConfiguration; |
| |
| /** |
| * Facilitates sending of motion events to a {@link UiController}. |
| */ |
| final class MotionEvents { |
| |
| private static final String TAG = MotionEvents.class.getSimpleName(); |
| |
| @VisibleForTesting |
| static final int MAX_CLICK_ATTEMPTS = 3; |
| |
| private MotionEvents() { |
| // Shouldn't be instantiated |
| } |
| |
| static DownResultHolder sendDown( |
| UiController uiController, float[] coordinates, float[] precision) { |
| checkNotNull(uiController); |
| checkNotNull(coordinates); |
| checkNotNull(precision); |
| |
| for (int retry = 0; retry < MAX_CLICK_ATTEMPTS; retry++) { |
| MotionEvent motionEvent = null; |
| try { |
| // Algorithm of sending click event adopted from android.test.TouchUtils. |
| // When the click event was first initiated. Needs to be same for both down and up press |
| // events. |
| long downTime = SystemClock.uptimeMillis(); |
| |
| // Down press. |
| motionEvent = MotionEvent.obtain(downTime, |
| SystemClock.uptimeMillis(), |
| MotionEvent.ACTION_DOWN, |
| coordinates[0], |
| coordinates[1], |
| 0, // pressure |
| 1, // size |
| 0, // metaState |
| precision[0], // xPrecision |
| precision[1], // yPrecision |
| 0, // deviceId |
| 0); // edgeFlags |
| // The down event should be considered a tap if it is long enough to be detected |
| // but short enough not to be a long-press. Assume that TapTimeout is set at least |
| // twice the detection time for a tap (no need to sleep for the whole TapTimeout since |
| // we aren't concerned about scrolling here). |
| long isTapAt = downTime + (ViewConfiguration.getTapTimeout() / 2); |
| |
| boolean injectEventSucceeded = uiController.injectMotionEvent(motionEvent); |
| |
| while (true) { |
| long delayToBeTap = isTapAt - SystemClock.uptimeMillis(); |
| if (delayToBeTap <= 10) { |
| break; |
| } |
| // 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(delayToBeTap / 4); |
| } |
| |
| boolean longPress = false; |
| if (SystemClock.uptimeMillis() > (downTime + ViewConfiguration.getLongPressTimeout())) { |
| longPress = true; |
| Log.e(TAG, "Overslept and turned a tap into a long press"); |
| UsageTrackerRegistry.getInstance().trackUsage("Espresso.Tap.Error.tapToLongPress"); |
| } |
| |
| if (!injectEventSucceeded) { |
| motionEvent.recycle(); |
| motionEvent = null; |
| continue; |
| } |
| |
| return new DownResultHolder(motionEvent, longPress); |
| } catch (InjectEventSecurityException e) { |
| throw new PerformException.Builder() |
| .withActionDescription("Send down montion event") |
| .withViewDescription("unknown") // likely to be replaced by FailureHandler |
| .withCause(e) |
| .build(); |
| } |
| } |
| throw new PerformException.Builder() |
| .withActionDescription(String.format("click (after %s attempts)", MAX_CLICK_ATTEMPTS)) |
| .withViewDescription("unknown") // likely to be replaced by FailureHandler |
| .build(); |
| } |
| |
| static boolean sendUp(UiController uiController, MotionEvent downEvent) { |
| return sendUp(uiController, downEvent, new float[] { downEvent.getX(), downEvent.getY() }); |
| } |
| |
| static boolean sendUp(UiController uiController, MotionEvent downEvent, float[] coordinates) { |
| checkNotNull(uiController); |
| checkNotNull(downEvent); |
| checkNotNull(coordinates); |
| |
| MotionEvent motionEvent = null; |
| try { |
| // Up press. |
| motionEvent = MotionEvent.obtain(downEvent.getDownTime(), |
| SystemClock.uptimeMillis(), |
| MotionEvent.ACTION_UP, |
| coordinates[0], |
| coordinates[1], |
| 0); |
| boolean injectEventSucceeded = uiController.injectMotionEvent(motionEvent); |
| |
| if (!injectEventSucceeded) { |
| Log.e(TAG, String.format( |
| "Injection of up event failed (corresponding down event: %s)", downEvent.toString())); |
| return false; |
| } |
| } catch (InjectEventSecurityException e) { |
| throw new PerformException.Builder() |
| .withActionDescription( |
| String.format("inject up event (corresponding down event: %s)", downEvent.toString())) |
| .withViewDescription("unknown") // likely to be replaced by FailureHandler |
| .withCause(e) |
| .build(); |
| } finally { |
| if (null != motionEvent) { |
| motionEvent.recycle(); |
| motionEvent = null; |
| } |
| } |
| return true; |
| } |
| |
| static void sendCancel(UiController uiController, MotionEvent downEvent) { |
| checkNotNull(uiController); |
| checkNotNull(downEvent); |
| |
| MotionEvent motionEvent = null; |
| try { |
| // Up press. |
| motionEvent = MotionEvent.obtain(downEvent.getDownTime(), |
| SystemClock.uptimeMillis(), |
| MotionEvent.ACTION_CANCEL, |
| downEvent.getX(), |
| downEvent.getY(), |
| 0); |
| boolean injectEventSucceeded = uiController.injectMotionEvent(motionEvent); |
| |
| if (!injectEventSucceeded) { |
| throw new PerformException.Builder() |
| .withActionDescription(String.format( |
| "inject cancel event (corresponding down event: %s)", downEvent.toString())) |
| .withViewDescription("unknown") // likely to be replaced by FailureHandler |
| .build(); |
| } |
| } catch (InjectEventSecurityException e) { |
| throw new PerformException.Builder() |
| .withActionDescription(String.format( |
| "inject cancel event (corresponding down event: %s)", downEvent.toString())) |
| .withViewDescription("unknown") // likely to be replaced by FailureHandler |
| .withCause(e) |
| .build(); |
| } finally { |
| if (null != motionEvent) { |
| motionEvent.recycle(); |
| motionEvent = null; |
| } |
| } |
| } |
| |
| static boolean sendMovement(UiController uiController, MotionEvent downEvent, |
| float[] coordinates) { |
| checkNotNull(uiController); |
| checkNotNull(downEvent); |
| checkNotNull(coordinates); |
| |
| MotionEvent motionEvent = null; |
| try { |
| motionEvent = MotionEvent.obtain(downEvent.getDownTime(), |
| SystemClock.uptimeMillis(), |
| MotionEvent.ACTION_MOVE, |
| coordinates[0], |
| coordinates[1], |
| 0); |
| boolean injectEventSucceeded = uiController.injectMotionEvent(motionEvent); |
| |
| if (!injectEventSucceeded) { |
| Log.e(TAG, String.format( |
| "Injection of motion event failed (corresponding down event: %s)", |
| downEvent.toString())); |
| return false; |
| } |
| } catch (InjectEventSecurityException e) { |
| throw new PerformException.Builder() |
| .withActionDescription(String.format( |
| "inject motion event (corresponding down event: %s)", downEvent.toString())) |
| .withViewDescription("unknown") // likely to be replaced by FailureHandler |
| .withCause(e) |
| .build(); |
| } finally { |
| if (null != motionEvent) { |
| motionEvent.recycle(); |
| motionEvent = null; |
| } |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Holds the result of a down motion. |
| */ |
| static class DownResultHolder { |
| public final MotionEvent down; |
| public final boolean longPress; |
| |
| DownResultHolder(MotionEvent down, boolean longPress) { |
| this.down = down; |
| this.longPress = longPress; |
| } |
| } |
| |
| } |