.holdGestureResult() =
* @param delay Int
* @return Boolean
*/
-@RequiresApi(Build.VERSION_CODES.N)
suspend fun pressWithTime(x: Int, y: Int, delay: Int) =
gesture(delay.toLong(), arrayOf(Pair(x, y)))
@@ -246,7 +215,6 @@ suspend fun pressWithTime(x: Int, y: Int, delay: Int) =
* @param y Int
* @return Boolean
*/
-@RequiresApi(Build.VERSION_CODES.N)
suspend fun longClick(x: Int, y: Int) = pressWithTime(
x, y, (ViewConfiguration.getLongPressTimeout() + 200)
)
@@ -260,7 +228,6 @@ suspend fun longClick(x: Int, y: Int) = pressWithTime(
* @param dur Int
* @return Boolean
*/
-@RequiresApi(Build.VERSION_CODES.N)
suspend fun swipe(
x1: Int, y1: Int,
x2: Int, y2: Int,
@@ -271,15 +238,13 @@ suspend fun swipe(
Pair(x2, y2)
))
-@RequiresApi(Build.VERSION_CODES.N)
-suspend fun scrollUp(): Boolean = runCatching {
+suspend fun scrollUp(): Boolean {
val mtop = (ScreenAdapter.relHeight * 0.1).toInt()
val mBottom = (ScreenAdapter.relHeight * 0.85).toInt()
val xCenter = (ScreenAdapter.relWidth * 0.5).toInt()
return swipe(xCenter, mBottom, xCenter, mtop, 400)
-}.holdGestureResult()
+}
-@RequiresApi(Build.VERSION_CODES.N)
suspend fun scrollDown(): Boolean {
val mtop = (ScreenAdapter.relHeight * 0.15).toInt()
val mBottom = (ScreenAdapter.relHeight * 0.9).toInt()
diff --git a/core/src/main/java/cn/vove7/auto/core/api/nav_api.kt b/core/src/main/java/cn/vove7/auto/core/api/nav_api.kt
new file mode 100644
index 0000000..a45ebea
--- /dev/null
+++ b/core/src/main/java/cn/vove7/auto/core/api/nav_api.kt
@@ -0,0 +1,48 @@
+package cn.vove7.auto.core.api
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import cn.vove7.auto.core.AppScope
+import cn.vove7.auto.core.AutoApi
+import cn.vove7.auto.core.utils.whileWaitTime
+import kotlin.math.min
+
+/**
+ * # nav_api
+ *
+ * Created on 2020/6/10
+ * @author Vove
+ */
+
+fun back(): Boolean = AutoApi.back()
+
+fun home(): Boolean = AutoApi.home()
+
+fun powerDialog(): Boolean = AutoApi.powerDialog()
+
+fun pullNotificationBar(): Boolean =
+ AutoApi.notificationBar()
+
+fun quickSettings(): Boolean = AutoApi.quickSettings()
+
+fun recents(): Boolean = AutoApi.recents()
+
+@RequiresApi(Build.VERSION_CODES.P)
+fun lockScreen(): Boolean = AutoApi.lockScreen()
+
+@RequiresApi(Build.VERSION_CODES.P)
+fun screenShot(): Boolean = AutoApi.screenShot()
+
+@RequiresApi(Build.VERSION_CODES.N)
+fun splitScreen(): Boolean = AutoApi.splitScreen()
+
+suspend fun waitForApp(pkg: String, waitTime: Long = 30000): Boolean {
+ return waitForPage(AppScope(pkg, ""), waitTime)
+}
+
+suspend fun waitForPage(scope: AppScope, waitTime: Long = 30000): Boolean {
+ return whileWaitTime(min(waitTime, 30000), 100) {
+ if (AutoApi.currentScope == scope) true
+ else null
+ } ?: false
+}
diff --git a/core/src/main/java/cn/vove7/auto/core/utils/AutoGestureDescription.java b/core/src/main/java/cn/vove7/auto/core/utils/AutoGestureDescription.java
new file mode 100644
index 0000000..c41ee41
--- /dev/null
+++ b/core/src/main/java/cn/vove7/auto/core/utils/AutoGestureDescription.java
@@ -0,0 +1,606 @@
+/*
+ * Copyright (C) 2015 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 cn.vove7.auto.core.utils;
+
+import android.annotation.SuppressLint;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.Display;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.core.util.Preconditions;
+
+/**
+ * This class describes those gestures. Gestures are made up of one or more strokes.
+ * Gestures are immutable once built and will be dispatched to the specified display.
+ *
+ * Spatial dimensions throughout are in screen pixels. Time is measured in milliseconds.
+ */
+public final class AutoGestureDescription {
+ /**
+ * Gestures may contain no more than this many strokes
+ */
+ private static final int MAX_STROKE_COUNT = 20;
+
+ /**
+ * Upper bound on total gesture duration. Nearly all gestures will be much shorter.
+ */
+ private static final long MAX_GESTURE_DURATION_MS = 60 * 1000;
+
+ private final List mStrokes = new ArrayList<>();
+ private final float[] mTempPos = new float[2];
+ private final int mDisplayId;
+
+ /**
+ * Get the upper limit for the number of strokes a gesture may contain.
+ *
+ * @return The maximum number of strokes.
+ */
+ public static int getMaxStrokeCount() {
+ return MAX_STROKE_COUNT;
+ }
+
+ /**
+ * Get the upper limit on a gesture's duration.
+ *
+ * @return The maximum duration in milliseconds.
+ */
+ public static long getMaxGestureDuration() {
+ return MAX_GESTURE_DURATION_MS;
+ }
+
+ private AutoGestureDescription() {
+ this(new ArrayList<>());
+ }
+
+ private AutoGestureDescription(List strokes) {
+ this(strokes, Display.DEFAULT_DISPLAY);
+ }
+
+ private AutoGestureDescription(List strokes, int displayId) {
+ mStrokes.addAll(strokes);
+ mDisplayId = displayId;
+ }
+
+ /**
+ * Get the number of stroke in the gesture.
+ *
+ * @return the number of strokes in this gesture
+ */
+ public int getStrokeCount() {
+ return mStrokes.size();
+ }
+
+ /**
+ * Read a stroke from the gesture
+ *
+ * @param index the index of the stroke
+ * @return A description of the stroke.
+ */
+ public StrokeDescription getStroke(@IntRange(from = 0) int index) {
+ return mStrokes.get(index);
+ }
+
+ /**
+ * Returns the ID of the display this gesture is sent on, for use with
+ * {@link android.hardware.display.DisplayManager#getDisplay(int)}.
+ *
+ * @return The logical display id.
+ */
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
+ /**
+ * Return the smallest key point (where a path starts or ends) that is at least a specified
+ * offset
+ *
+ * @param offset the minimum start time
+ * @return The next key time that is at least the offset or -1 if one can't be found
+ */
+ private long getNextKeyPointAtLeast(long offset) {
+ long nextKeyPoint = Long.MAX_VALUE;
+ for (int i = 0; i < mStrokes.size(); i++) {
+ long thisStartTime = mStrokes.get(i).mStartTime;
+ if ((thisStartTime < nextKeyPoint) && (thisStartTime >= offset)) {
+ nextKeyPoint = thisStartTime;
+ }
+ long thisEndTime = mStrokes.get(i).mEndTime;
+ if ((thisEndTime < nextKeyPoint) && (thisEndTime >= offset)) {
+ nextKeyPoint = thisEndTime;
+ }
+ }
+ return (nextKeyPoint == Long.MAX_VALUE) ? -1L : nextKeyPoint;
+ }
+
+ /**
+ * Get the points that correspond to a particular moment in time.
+ *
+ * @param time The time of interest
+ * @param touchPoints An array to hold the current touch points. Must be preallocated to at
+ * least the number of paths in the gesture to prevent going out of bounds
+ * @return The number of points found, and thus the number of elements set in each array
+ */
+ private int getPointsForTime(long time, TouchPoint[] touchPoints) {
+ int numPointsFound = 0;
+ for (int i = 0; i < mStrokes.size(); i++) {
+ StrokeDescription strokeDescription = mStrokes.get(i);
+ if (strokeDescription.hasPointForTime(time)) {
+ touchPoints[numPointsFound].mStrokeId = strokeDescription.getId();
+ touchPoints[numPointsFound].mContinuedStrokeId =
+ strokeDescription.getContinuedStrokeId();
+ touchPoints[numPointsFound].mIsStartOfPath =
+ (strokeDescription.getContinuedStrokeId() < 0)
+ && (time == strokeDescription.mStartTime);
+ touchPoints[numPointsFound].mIsEndOfPath = !strokeDescription.willContinue()
+ && (time == strokeDescription.mEndTime);
+ strokeDescription.getPosForTime(time, mTempPos);
+ touchPoints[numPointsFound].mX = Math.round(mTempPos[0]);
+ touchPoints[numPointsFound].mY = Math.round(mTempPos[1]);
+ numPointsFound++;
+ }
+ }
+ return numPointsFound;
+ }
+
+ // Total duration assumes that the gesture starts at 0; waiting around to start a gesture
+ // counts against total duration
+ private static long getTotalDuration(List paths) {
+ long latestEnd = Long.MIN_VALUE;
+ for (int i = 0; i < paths.size(); i++) {
+ StrokeDescription path = paths.get(i);
+ latestEnd = Math.max(latestEnd, path.mEndTime);
+ }
+ return Math.max(latestEnd, 0);
+ }
+
+ /**
+ * Builder for a {@code GestureDescription}
+ */
+ public static class Builder {
+
+ private final List mStrokes = new ArrayList<>();
+ private int mDisplayId = Display.DEFAULT_DISPLAY;
+
+ /**
+ * Adds a stroke to the gesture description. Up to
+ * {@link AutoGestureDescription#getMaxStrokeCount()} paths may be
+ * added to a gesture, and the total gesture duration (earliest path start time to latest
+ * path end time) may not exceed {@link AutoGestureDescription#getMaxGestureDuration()}.
+ *
+ * @param strokeDescription the stroke to add.
+ * @return this
+ */
+ public Builder addStroke(@NonNull StrokeDescription strokeDescription) {
+ if (mStrokes.size() >= MAX_STROKE_COUNT) {
+ throw new IllegalStateException(
+ "Attempting to add too many strokes to a gesture. Maximum is "
+ + MAX_STROKE_COUNT
+ + ", got "
+ + mStrokes.size());
+ }
+
+ mStrokes.add(strokeDescription);
+
+ if (getTotalDuration(mStrokes) > MAX_GESTURE_DURATION_MS) {
+ mStrokes.remove(strokeDescription);
+ throw new IllegalStateException(
+ "Gesture would exceed maximum duration with new stroke");
+ }
+ return this;
+ }
+
+ /**
+ * Sets the id of the display to dispatch gestures.
+ *
+ * @param displayId The logical display id
+ * @return this
+ */
+ public @NonNull Builder setDisplayId(int displayId) {
+ mDisplayId = displayId;
+ return this;
+ }
+
+ public AutoGestureDescription build() {
+ if (mStrokes.size() == 0) {
+ throw new IllegalStateException("Gestures must have at least one stroke");
+ }
+ return new AutoGestureDescription(mStrokes, mDisplayId);
+ }
+ }
+
+ /**
+ * Immutable description of stroke that can be part of a gesture.
+ */
+ public static class StrokeDescription {
+ private static final int INVALID_STROKE_ID = -1;
+
+ static int sIdCounter;
+
+ Path mPath;
+ long mStartTime;
+ long mEndTime;
+ private float mTimeToLengthConversion;
+ private PathMeasure mPathMeasure;
+ // The tap location is only set for zero-length paths
+ float[] mTapLocation;
+ int mId;
+ boolean mContinued;
+ int mContinuedStrokeId = INVALID_STROKE_ID;
+
+ /**
+ * @param path The path to follow. Must have exactly one contour. The bounds of the path
+ * must not be negative. The path must not be empty. If the path has zero length
+ * (for example, a single {@code moveTo()}), the stroke is a touch that doesn't move.
+ * @param startTime The time, in milliseconds, from the time the gesture starts to the
+ * time the stroke should start. Must not be negative.
+ * @param duration The duration, in milliseconds, the stroke takes to traverse the path.
+ * Must be positive.
+ */
+ public StrokeDescription(@NonNull Path path,
+ @IntRange(from = 0) long startTime,
+ @IntRange(from = 0) long duration) {
+ this(path, startTime, duration, false);
+ }
+
+ /**
+ * @param path The path to follow. Must have exactly one contour. The bounds of the path
+ * must not be negative. The path must not be empty. If the path has zero length
+ * (for example, a single {@code moveTo()}), the stroke is a touch that doesn't move.
+ * @param startTime The time, in milliseconds, from the time the gesture starts to the
+ * time the stroke should start. Must not be negative.
+ * @param duration The duration, in milliseconds, the stroke takes to traverse the path.
+ * Must be positive.
+ * @param willContinue {@code true} if this stroke will be continued by one in the
+ * next gesture {@code false} otherwise. Continued strokes keep their pointers down when
+ * the gesture completes.
+ */
+ @SuppressLint("RestrictedApi")
+ public StrokeDescription(@NonNull Path path,
+ @IntRange(from = 0) long startTime,
+ @IntRange(from = 0) long duration,
+ boolean willContinue) {
+ mContinued = willContinue;
+ Preconditions.checkArgument(duration > 0, "Duration must be positive");
+ Preconditions.checkArgument(startTime >= 0, "Start time must not be negative");
+ Preconditions.checkArgument(!path.isEmpty(), "Path is empty");
+ RectF bounds = new RectF();
+ path.computeBounds(bounds, false /* unused */);
+ Preconditions.checkArgument((bounds.bottom >= 0) && (bounds.top >= 0)
+ && (bounds.right >= 0) && (bounds.left >= 0),
+ "Path bounds must not be negative");
+ mPath = new Path(path);
+ mPathMeasure = new PathMeasure(path, false);
+ if (mPathMeasure.getLength() == 0) {
+ // Treat zero-length paths as taps
+ Path tempPath = new Path(path);
+ tempPath.lineTo(-1, -1);
+ mTapLocation = new float[2];
+ PathMeasure pathMeasure = new PathMeasure(tempPath, false);
+ pathMeasure.getPosTan(0, mTapLocation, null);
+ }
+ if (mPathMeasure.nextContour()) {
+ throw new IllegalArgumentException("Path has more than one contour");
+ }
+ /*
+ * Calling nextContour has moved mPathMeasure off the first contour, which is the only
+ * one we care about. Set the path again to go back to the first contour.
+ */
+ mPathMeasure.setPath(mPath, false);
+ mStartTime = startTime;
+ mEndTime = startTime + duration;
+ mTimeToLengthConversion = getLength() / duration;
+ mId = sIdCounter++;
+ }
+
+ /**
+ * Retrieve a copy of the path for this stroke
+ *
+ * @return A copy of the path
+ */
+ public Path getPath() {
+ return new Path(mPath);
+ }
+
+ /**
+ * Get the stroke's start time
+ *
+ * @return the start time for this stroke.
+ */
+ public long getStartTime() {
+ return mStartTime;
+ }
+
+ /**
+ * Get the stroke's duration
+ *
+ * @return the duration for this stroke
+ */
+ public long getDuration() {
+ return mEndTime - mStartTime;
+ }
+
+ /**
+ * Get the stroke's ID. The ID is used when a stroke is to be continued by another
+ * stroke in a future gesture.
+ *
+ * @return the ID of this stroke
+ * @hide
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Create a new stroke that will continue this one. This is only possible if this stroke
+ * will continue.
+ *
+ * @param path The path for the stroke that continues this one. The starting point of
+ * this path must match the ending point of the stroke it continues.
+ * @param startTime The time, in milliseconds, from the time the gesture starts to the
+ * time this stroke should start. Must not be negative. This time is from
+ * the start of the new gesture, not the one being continued.
+ * @param duration The duration for the new stroke. Must not be negative.
+ * @param willContinue {@code true} if this stroke will be continued by one in the
+ * next gesture {@code false} otherwise.
+ * @return
+ */
+ public StrokeDescription continueStroke(Path path, long startTime, long duration,
+ boolean willContinue) {
+ if (!mContinued) {
+ throw new IllegalStateException(
+ "Only strokes marked willContinue can be continued");
+ }
+ StrokeDescription strokeDescription =
+ new StrokeDescription(path, startTime, duration, willContinue);
+ strokeDescription.mContinuedStrokeId = mId;
+ return strokeDescription;
+ }
+
+ /**
+ * Check if this stroke is marked to continue in the next gesture.
+ *
+ * @return {@code true} if the stroke is to be continued.
+ */
+ public boolean willContinue() {
+ return mContinued;
+ }
+
+ /**
+ * Get the ID of the stroke that this one will continue.
+ *
+ * @return The ID of the stroke that this stroke continues, or 0 if no such stroke exists.
+ * @hide
+ */
+ public int getContinuedStrokeId() {
+ return mContinuedStrokeId;
+ }
+
+ float getLength() {
+ return mPathMeasure.getLength();
+ }
+
+ /* Assumes hasPointForTime returns true */
+ boolean getPosForTime(long time, float[] pos) {
+ if (mTapLocation != null) {
+ pos[0] = mTapLocation[0];
+ pos[1] = mTapLocation[1];
+ return true;
+ }
+ if (time == mEndTime) {
+ // Close to the end time, roundoff can be a problem
+ return mPathMeasure.getPosTan(getLength(), pos, null);
+ }
+ float length = mTimeToLengthConversion * ((float) (time - mStartTime));
+ return mPathMeasure.getPosTan(length, pos, null);
+ }
+
+ boolean hasPointForTime(long time) {
+ return ((time >= mStartTime) && (time <= mEndTime));
+ }
+ }
+
+ /**
+ * The location of a finger for gesture dispatch
+ *
+ * @hide
+ */
+ public static class TouchPoint implements Parcelable {
+ private static final int FLAG_IS_START_OF_PATH = 0x01;
+ private static final int FLAG_IS_END_OF_PATH = 0x02;
+
+ public int mStrokeId;
+ public int mContinuedStrokeId;
+ public boolean mIsStartOfPath;
+ public boolean mIsEndOfPath;
+ public float mX;
+ public float mY;
+
+ public TouchPoint() {
+ }
+
+ public TouchPoint(TouchPoint pointToCopy) {
+ copyFrom(pointToCopy);
+ }
+
+ public TouchPoint(Parcel parcel) {
+ mStrokeId = parcel.readInt();
+ mContinuedStrokeId = parcel.readInt();
+ int startEnd = parcel.readInt();
+ mIsStartOfPath = (startEnd & FLAG_IS_START_OF_PATH) != 0;
+ mIsEndOfPath = (startEnd & FLAG_IS_END_OF_PATH) != 0;
+ mX = parcel.readFloat();
+ mY = parcel.readFloat();
+ }
+
+ public void copyFrom(TouchPoint other) {
+ mStrokeId = other.mStrokeId;
+ mContinuedStrokeId = other.mContinuedStrokeId;
+ mIsStartOfPath = other.mIsStartOfPath;
+ mIsEndOfPath = other.mIsEndOfPath;
+ mX = other.mX;
+ mY = other.mY;
+ }
+
+ @Override
+ public String toString() {
+ return "TouchPoint{"
+ + "mStrokeId=" + mStrokeId
+ + ", mContinuedStrokeId=" + mContinuedStrokeId
+ + ", mIsStartOfPath=" + mIsStartOfPath
+ + ", mIsEndOfPath=" + mIsEndOfPath
+ + ", mX=" + mX
+ + ", mY=" + mY
+ + '}';
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mStrokeId);
+ dest.writeInt(mContinuedStrokeId);
+ int startEnd = mIsStartOfPath ? FLAG_IS_START_OF_PATH : 0;
+ startEnd |= mIsEndOfPath ? FLAG_IS_END_OF_PATH : 0;
+ dest.writeInt(startEnd);
+ dest.writeFloat(mX);
+ dest.writeFloat(mY);
+ }
+
+ public static final @NonNull Parcelable.Creator CREATOR
+ = new Parcelable.Creator() {
+ public TouchPoint createFromParcel(Parcel in) {
+ return new TouchPoint(in);
+ }
+
+ public TouchPoint[] newArray(int size) {
+ return new TouchPoint[size];
+ }
+ };
+ }
+
+ /**
+ * A step along a gesture. Contains all of the touch points at a particular time
+ *
+ * @hide
+ */
+ public static class GestureStep implements Parcelable {
+ public long timeSinceGestureStart;
+ public int numTouchPoints;
+ public TouchPoint[] touchPoints;
+
+ public GestureStep(long timeSinceGestureStart, int numTouchPoints,
+ TouchPoint[] touchPointsToCopy) {
+ this.timeSinceGestureStart = timeSinceGestureStart;
+ this.numTouchPoints = numTouchPoints;
+ this.touchPoints = new TouchPoint[numTouchPoints];
+ for (int i = 0; i < numTouchPoints; i++) {
+ this.touchPoints[i] = new TouchPoint(touchPointsToCopy[i]);
+ }
+ }
+
+ public GestureStep(Parcel parcel) {
+ timeSinceGestureStart = parcel.readLong();
+ Parcelable[] parcelables =
+ parcel.readParcelableArray(TouchPoint.class.getClassLoader());
+ numTouchPoints = (parcelables == null) ? 0 : parcelables.length;
+ touchPoints = new TouchPoint[numTouchPoints];
+ for (int i = 0; i < numTouchPoints; i++) {
+ touchPoints[i] = (TouchPoint) parcelables[i];
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(timeSinceGestureStart);
+ dest.writeParcelableArray(touchPoints, flags);
+ }
+
+ public static final @NonNull Parcelable.Creator CREATOR
+ = new Parcelable.Creator() {
+ public GestureStep createFromParcel(Parcel in) {
+ return new GestureStep(in);
+ }
+
+ public GestureStep[] newArray(int size) {
+ return new GestureStep[size];
+ }
+ };
+ }
+
+ /**
+ * Class to convert a GestureDescription to a series of GestureSteps.
+ *
+ * @hide
+ */
+ public static class MotionEventGenerator {
+ /* Lazily-created scratch memory for processing touches */
+ private static TouchPoint[] sCurrentTouchPoints;
+
+ public static List getGestureStepsFromGestureDescription(
+ AutoGestureDescription description, int sampleTimeMs) {
+ final List gestureSteps = new ArrayList<>();
+
+ // Point data at each time we generate an event for
+ final TouchPoint[] currentTouchPoints =
+ getCurrentTouchPoints(description.getStrokeCount());
+ int currentTouchPointSize = 0;
+ /* Loop through each time slice where there are touch points */
+ long timeSinceGestureStart = 0;
+ long nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart);
+ while (nextKeyPointTime >= 0) {
+ timeSinceGestureStart = (currentTouchPointSize == 0) ? nextKeyPointTime
+ : Math.min(nextKeyPointTime, timeSinceGestureStart + sampleTimeMs);
+ currentTouchPointSize = description.getPointsForTime(timeSinceGestureStart,
+ currentTouchPoints);
+ gestureSteps.add(new GestureStep(timeSinceGestureStart, currentTouchPointSize,
+ currentTouchPoints));
+
+ /* Move to next time slice */
+ nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart + 1);
+ }
+ return gestureSteps;
+ }
+
+ private static TouchPoint[] getCurrentTouchPoints(int requiredCapacity) {
+ if ((sCurrentTouchPoints == null) || (sCurrentTouchPoints.length < requiredCapacity)) {
+ sCurrentTouchPoints = new TouchPoint[requiredCapacity];
+ for (int i = 0; i < requiredCapacity; i++) {
+ sCurrentTouchPoints[i] = new TouchPoint();
+ }
+ }
+ return sCurrentTouchPoints;
+ }
+ }
+}
diff --git a/core/src/main/java/cn/vove7/auto/core/utils/GestureResultCallback.java b/core/src/main/java/cn/vove7/auto/core/utils/GestureResultCallback.java
new file mode 100644
index 0000000..384800f
--- /dev/null
+++ b/core/src/main/java/cn/vove7/auto/core/utils/GestureResultCallback.java
@@ -0,0 +1,19 @@
+package cn.vove7.auto.core.utils;
+
+public abstract class GestureResultCallback {
+ /**
+ * Called when the gesture has completed successfully
+ *
+ * @param gestureDescription The description of the gesture that completed.
+ */
+ public void onCompleted(AutoGestureDescription gestureDescription) {
+ }
+
+ /**
+ * Called when the gesture was cancelled
+ *
+ * @param gestureDescription The description of the gesture that was cancelled.
+ */
+ public void onCancelled(AutoGestureDescription gestureDescription) {
+ }
+}
\ No newline at end of file
diff --git a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/utils/ScreenAdapter.kt b/core/src/main/java/cn/vove7/auto/core/utils/ScreenAdapter.kt
similarity index 93%
rename from accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/utils/ScreenAdapter.kt
rename to core/src/main/java/cn/vove7/auto/core/utils/ScreenAdapter.kt
index 71b9167..90823e0 100644
--- a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/utils/ScreenAdapter.kt
+++ b/core/src/main/java/cn/vove7/auto/core/utils/ScreenAdapter.kt
@@ -1,4 +1,4 @@
-package cn.vove7.andro_accessibility_api.utils
+package cn.vove7.auto.core.utils
import android.content.Context
import android.graphics.Point
@@ -7,7 +7,7 @@ import android.os.Build
import android.util.DisplayMetrics
import android.util.Pair
import android.view.WindowManager
-import cn.vove7.andro_accessibility_api.AccessibilityApi
+import cn.vove7.auto.core.AutoApi
/**
* # ScreenAdapter
@@ -20,7 +20,7 @@ object ScreenAdapter {
private val deviceWidth: Int
init {
- val display = (AccessibilityApi.appCtx.getSystemService(Context.WINDOW_SERVICE) as WindowManager)
+ val display = (AutoApi.appCtx.getSystemService(Context.WINDOW_SERVICE) as WindowManager)
.defaultDisplay
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 -> {
@@ -65,7 +65,6 @@ object ScreenAdapter {
}
fun scalePoints(points: Array>): Array> {
-
val ps = Array(points.size) { Pair(0f, 0f) }
val index = 0
diff --git a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/utils/ViewChildArray.kt b/core/src/main/java/cn/vove7/auto/core/utils/ViewChildArray.kt
similarity index 87%
rename from accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/utils/ViewChildArray.kt
rename to core/src/main/java/cn/vove7/auto/core/utils/ViewChildArray.kt
index d9fb07d..f348b66 100644
--- a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/utils/ViewChildArray.kt
+++ b/core/src/main/java/cn/vove7/auto/core/utils/ViewChildArray.kt
@@ -1,6 +1,6 @@
-package cn.vove7.andro_accessibility_api.utils
+package cn.vove7.auto.core.utils
-import cn.vove7.andro_accessibility_api.viewnode.ViewNode
+import cn.vove7.auto.core.viewnode.ViewNode
/**
* # ViewChildArray
diff --git a/core/src/main/java/cn/vove7/auto/core/utils/exceptions.kt b/core/src/main/java/cn/vove7/auto/core/utils/exceptions.kt
new file mode 100644
index 0000000..01f2a18
--- /dev/null
+++ b/core/src/main/java/cn/vove7/auto/core/utils/exceptions.kt
@@ -0,0 +1,31 @@
+package cn.vove7.auto.core.utils
+
+import cn.vove7.auto.core.viewfinder.ViewFinder
+
+/**
+ * # exceptions
+ * 异常类合集
+ * @author Administrator
+ * 2018/12/20
+ */
+
+/**
+ * 视图搜索失败异常
+ */
+class ViewNodeNotFoundException : Exception {
+ constructor(finder: ViewFinder<*>)
+ : super("ViewNodeNotFound: ${finder.finderInfo()}")
+
+ constructor(msg: String) : super(msg)
+}
+
+class GestureCanceledException(
+ val gestureDescription: AutoGestureDescription
+) : RuntimeException()
+
+class AutoServiceUnavailableException : RuntimeException() {
+
+ override fun toString(): String {
+ return "AutoServiceUnavailableException"
+ }
+}
diff --git a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/utils/utils.kt b/core/src/main/java/cn/vove7/auto/core/utils/utils.kt
similarity index 50%
rename from accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/utils/utils.kt
rename to core/src/main/java/cn/vove7/auto/core/utils/utils.kt
index 15e396b..c4d7c31 100644
--- a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/utils/utils.kt
+++ b/core/src/main/java/cn/vove7/auto/core/utils/utils.kt
@@ -1,15 +1,19 @@
-package cn.vove7.andro_accessibility_api.utils
+package cn.vove7.auto.core.utils
+import android.accessibilityservice.GestureDescription
+import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.os.Build
import android.os.Bundle
import android.os.SystemClock
import android.provider.Settings
-import cn.vove7.andro_accessibility_api.AccessibilityApi
+import androidx.annotation.RequiresApi
+import cn.vove7.auto.core.AutoApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
-import java.util.Locale
+import java.util.*
import kotlin.coroutines.coroutineContext
import kotlin.math.max
@@ -35,7 +39,7 @@ suspend fun whileWaitTime(
val begin = SystemClock.elapsedRealtime()
do {
run.invoke()?.also {
- //if 耗时操作
+ // if 耗时操作
return it
}
if (interval > 0) delay(interval)
@@ -52,11 +56,11 @@ internal suspend inline fun ensureActive() {
fun jumpAccessibilityServiceSettings(
cls: Class<*>,
- ctx: Context = AccessibilityApi.appCtx
+ ctx: Context = AutoApi.appCtx
) {
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- intent.putComponent(AccessibilityApi.appCtx.packageName, cls)
+ intent.putComponent(AutoApi.appCtx.packageName, cls)
ctx.startActivity(intent)
}
@@ -75,19 +79,19 @@ fun compareSimilarity(str1: String, str2: String, ignoreCase: Boolean = true): F
s1 = str1.lowercase(Locale.getDefault())
s2 = str2.lowercase(Locale.getDefault())
}
- //计算两个字符串的长度
+ // 计算两个字符串的长度
val len1 = s1.length
val len2 = s2.length
- //建立上面说的数组,比字符长度大一个空间
+ // 建立上面说的数组,比字符长度大一个空间
val dif = Array(len1 + 1) { IntArray(len2 + 1) }
- //赋初值,步骤B
+ // 赋初值,步骤B
for (a in 0..len1) {
dif[a][0] = a
}
for (a in 0..len2) {
dif[0][a] = a
}
- //计算两个字符是否一样,计算左上的值
+ // 计算两个字符是否一样,计算左上的值
var temp: Int
for (i in 1..len1) {
for (j in 1..len2) {
@@ -96,7 +100,7 @@ fun compareSimilarity(str1: String, str2: String, ignoreCase: Boolean = true): F
} else {
1
}
- //取三个值中最小的
+ // 取三个值中最小的
dif[i][j] = arrayOf(
dif[i - 1][j - 1] + temp, dif[i][j - 1] + 1,
dif[i - 1][j] + 1
@@ -112,4 +116,53 @@ operator fun String.times(number: Int): String {
append(this@times)
}
}
+}
+
+@SuppressLint("PrivateApi")
+fun getApplication(): Context {
+ val clazz = Class.forName("android.app.ActivityThread")
+ val currentActivityThread = clazz.getMethod("currentActivityThread").apply {
+ isAccessible = true
+ }
+ val getApplication = clazz.getMethod("getApplication").apply {
+ isAccessible = true
+ }
+ val activityThread = currentActivityThread.invoke(null)
+ return getApplication.invoke(activityThread) as Context
+}
+
+
+@RequiresApi(Build.VERSION_CODES.N)
+fun AutoGestureDescription.convert(): GestureDescription {
+ val autoDesc = this
+ return GestureDescription.Builder()
+ .apply {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ setDisplayId(autoDesc.displayId)
+ }
+ for (i in 0 until autoDesc.strokeCount) {
+ val autoStroke = autoDesc.getStroke(i)
+ val stroke = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ GestureDescription.StrokeDescription(autoStroke.path, autoStroke.startTime,
+ autoStroke.duration, autoStroke.willContinue()).also {
+ it::class.java.getDeclaredField("mContinuedStrokeId").also { f ->
+ f.isAccessible = true
+ f.set(it, autoStroke.mContinuedStrokeId)
+ }
+ }
+ } else {
+ GestureDescription.StrokeDescription(autoStroke.path, autoStroke.startTime,
+ autoStroke.duration)
+ }
+ kotlin.runCatching {
+ stroke::class.java.getDeclaredField("mId").also { f ->
+ f.isAccessible = true
+ f.set(stroke, autoStroke.id)
+ }
+ }
+ addStroke(stroke)
+ }
+ }
+ .build()
+
}
\ No newline at end of file
diff --git a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewfinder/FinderBuilderWithOperation.kt b/core/src/main/java/cn/vove7/auto/core/viewfinder/FinderBuilderWithOperation.kt
similarity index 97%
rename from accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewfinder/FinderBuilderWithOperation.kt
rename to core/src/main/java/cn/vove7/auto/core/viewfinder/FinderBuilderWithOperation.kt
index 0e4bba7..bf6d514 100644
--- a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewfinder/FinderBuilderWithOperation.kt
+++ b/core/src/main/java/cn/vove7/auto/core/viewfinder/FinderBuilderWithOperation.kt
@@ -1,9 +1,9 @@
-package cn.vove7.andro_accessibility_api.viewfinder
+package cn.vove7.auto.core.viewfinder
import android.os.Build
import android.view.accessibility.AccessibilityNodeInfo
import androidx.annotation.RequiresApi
-import cn.vove7.andro_accessibility_api.viewnode.ViewOperation
+import cn.vove7.auto.core.viewnode.ViewOperation
import kotlinx.coroutines.runBlocking
/**
diff --git a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/ext/ScreenTextFinder.kt b/core/src/main/java/cn/vove7/auto/core/viewfinder/ScreenTextFinder.kt
similarity index 61%
rename from accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/ext/ScreenTextFinder.kt
rename to core/src/main/java/cn/vove7/auto/core/viewfinder/ScreenTextFinder.kt
index 690b1df..dddf7c9 100644
--- a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/ext/ScreenTextFinder.kt
+++ b/core/src/main/java/cn/vove7/auto/core/viewfinder/ScreenTextFinder.kt
@@ -1,8 +1,7 @@
-package cn.vove7.andro_accessibility_api.ext
+package cn.vove7.auto.core.viewfinder
import android.view.accessibility.AccessibilityNodeInfo
-import cn.vove7.andro_accessibility_api.viewfinder.ViewFinder
-import cn.vove7.andro_accessibility_api.viewnode.ViewNode
+import cn.vove7.auto.core.viewnode.ViewNode
/**
* # ScreenTextFinder
@@ -22,8 +21,8 @@ class ScreenTextFinder(
isWeb = true
return false
}
- return node.childCount == 0 && (node.text != null && node.text.trim() != "")
- || (isWeb && node.contentDescription ?: "" != "")
+ return ((node.childCount == 0) && (node.text != null && node.text.trim() != "")
+ || (isWeb && (node.contentDescription ?: "") != ""))
}
}
\ No newline at end of file
diff --git a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewfinder/SmartFinder.kt b/core/src/main/java/cn/vove7/auto/core/viewfinder/SmartFinder.kt
similarity index 87%
rename from accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewfinder/SmartFinder.kt
rename to core/src/main/java/cn/vove7/auto/core/viewfinder/SmartFinder.kt
index 16edd53..f27f5ba 100644
--- a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewfinder/SmartFinder.kt
+++ b/core/src/main/java/cn/vove7/auto/core/viewfinder/SmartFinder.kt
@@ -1,7 +1,7 @@
-package cn.vove7.andro_accessibility_api.viewfinder
+package cn.vove7.auto.core.viewfinder
import android.view.accessibility.AccessibilityNodeInfo
-import cn.vove7.andro_accessibility_api.viewnode.ViewNode
+import cn.vove7.auto.core.viewnode.ViewNode
/**
* # SmartFinder
@@ -12,7 +12,7 @@ import cn.vove7.andro_accessibility_api.viewnode.ViewNode
* @date 2022/2/15
*/
fun interface MatchCondition {
- var type: ConditionType
+ var conditionType: ConditionType
get() = ConditionType.AND
set(_) {}
@@ -22,7 +22,7 @@ fun interface MatchCondition {
enum class ConditionType { AND, OR }
class ConditionNode(
- override var type: ConditionType,
+ override var conditionType: ConditionType,
val condition: MatchCondition
) : MatchCondition by condition {
override fun toString() = condition.toString()
@@ -32,7 +32,7 @@ abstract class ConditionGroup(
node: ViewNode? = null
) : ViewFinder(node), MatchCondition, FinderBuilderWithOperation {
- override var type: ConditionType = ConditionType.AND
+ override var conditionType: ConditionType = ConditionType.AND
private val conditions: MutableList = mutableListOf()
private var lastType: ConditionType = ConditionType.AND
override val finder: ViewFinder<*> get() = this
@@ -41,7 +41,7 @@ abstract class ConditionGroup(
append("(")
conditions.forEachIndexed { i, cond ->
if (i > 0) {
- append(if (cond.type == ConditionType.AND) " && " else " || ")
+ append(if (cond.conditionType == ConditionType.AND) " && " else " || ")
}
append(cond.toString())
}
@@ -86,13 +86,13 @@ abstract class ConditionGroup(
}
infix fun and(group: ConditionGroup): ConditionGroup {
- group.type = ConditionType.AND
+ group.conditionType = ConditionType.AND
conditions.add(group)
return this
}
infix fun or(group: ConditionGroup): ConditionGroup {
- group.type = ConditionType.OR
+ group.conditionType = ConditionType.OR
conditions.add(group)
return this
}
@@ -101,17 +101,17 @@ abstract class ConditionGroup(
if (conditions.isEmpty()) {
throw IllegalArgumentException("SmartFinder has no conditions")
}
- if (conditions.first().type == ConditionType.OR) {
+ if (conditions.first().conditionType == ConditionType.OR) {
throw IllegalStateException("first condition type must be AND")
}
conditions.forEachIndexed { i, cond ->
if (cond.invoke(node)) {
- if (i + 1 < conditions.size && conditions[i + 1].type == ConditionType.OR) {
+ if (i + 1 < conditions.size && conditions[i + 1].conditionType == ConditionType.OR) {
//break true or (..)
return true
}
return@forEachIndexed
- } else if (i + 1 < conditions.size && conditions[i + 1].type == ConditionType.OR) {
+ } else if (i + 1 < conditions.size && conditions[i + 1].conditionType == ConditionType.OR) {
return@forEachIndexed
} else return false
}
diff --git a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewfinder/SmartFinderConditions.kt b/core/src/main/java/cn/vove7/auto/core/viewfinder/SmartFinderConditions.kt
similarity index 99%
rename from accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewfinder/SmartFinderConditions.kt
rename to core/src/main/java/cn/vove7/auto/core/viewfinder/SmartFinderConditions.kt
index 4c5cb9f..f5e6d6f 100644
--- a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewfinder/SmartFinderConditions.kt
+++ b/core/src/main/java/cn/vove7/auto/core/viewfinder/SmartFinderConditions.kt
@@ -1,11 +1,11 @@
@file:Suppress("unused")
-package cn.vove7.andro_accessibility_api.viewfinder
+package cn.vove7.auto.core.viewfinder
import android.os.Build
import android.view.accessibility.AccessibilityNodeInfo
import androidx.annotation.RequiresApi
-import cn.vove7.andro_accessibility_api.utils.compareSimilarity
+import cn.vove7.auto.core.utils.compareSimilarity
/**
* # SmartFinderConditions
diff --git a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewfinder/ViewFinder.kt b/core/src/main/java/cn/vove7/auto/core/viewfinder/ViewFinder.kt
similarity index 91%
rename from accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewfinder/ViewFinder.kt
rename to core/src/main/java/cn/vove7/auto/core/viewfinder/ViewFinder.kt
index fea49b2..8365605 100644
--- a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewfinder/ViewFinder.kt
+++ b/core/src/main/java/cn/vove7/auto/core/viewfinder/ViewFinder.kt
@@ -1,12 +1,11 @@
-package cn.vove7.andro_accessibility_api.viewfinder
+package cn.vove7.auto.core.viewfinder
import android.view.accessibility.AccessibilityNodeInfo
-import cn.vove7.andro_accessibility_api.api.requireBaseAccessibility
-import cn.vove7.andro_accessibility_api.utils.ViewNodeNotFoundException
-import cn.vove7.andro_accessibility_api.utils.ensureActive
-import cn.vove7.andro_accessibility_api.utils.whileWaitTime
-import cn.vove7.andro_accessibility_api.viewfinder.FinderBuilderWithOperation.Companion.WAIT_MILLIS
-import cn.vove7.andro_accessibility_api.viewnode.ViewNode
+import cn.vove7.auto.core.utils.ViewNodeNotFoundException
+import cn.vove7.auto.core.utils.ensureActive
+import cn.vove7.auto.core.utils.whileWaitTime
+import cn.vove7.auto.core.viewfinder.FinderBuilderWithOperation.Companion.WAIT_MILLIS
+import cn.vove7.auto.core.viewnode.ViewNode
import kotlinx.coroutines.delay
/**
@@ -63,7 +62,6 @@ abstract class ViewFinder>(
interval: Long = 20L,
includeInvisible: Boolean = false
): ViewNode? {
- requireBaseAccessibility()
val wt = when {
waitTime in 0..30000 -> waitTime
waitTime < 0 -> 0
@@ -100,7 +98,7 @@ abstract class ViewFinder>(
* @return ViewNode?
*/
suspend fun findByDepths(vararg depths: Int): ViewNode? {
- return Companion.findByDepths(depths, startNode)
+ return findByDepths(depths, startNode)
}
suspend fun requireByDepths(vararg depths: Int): ViewNode {
diff --git a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/api/view_finder_api.kt b/core/src/main/java/cn/vove7/auto/core/viewfinder/view_finder_api.kt
similarity index 75%
rename from accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/api/view_finder_api.kt
rename to core/src/main/java/cn/vove7/auto/core/viewfinder/view_finder_api.kt
index 69ee774..1dc93ba 100644
--- a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/api/view_finder_api.kt
+++ b/core/src/main/java/cn/vove7/auto/core/viewfinder/view_finder_api.kt
@@ -6,10 +6,9 @@ import android.os.Build
import android.util.Log
import android.view.accessibility.AccessibilityNodeInfo
import androidx.annotation.RequiresApi
-import cn.vove7.andro_accessibility_api.AccessibilityApi
-import cn.vove7.andro_accessibility_api.utils.times
-import cn.vove7.andro_accessibility_api.viewfinder.*
-import cn.vove7.andro_accessibility_api.viewnode.ViewNode
+import cn.vove7.auto.core.utils.times
+import cn.vove7.auto.core.viewfinder.*
+import cn.vove7.auto.core.viewnode.ViewNode
/**
* # apis
@@ -20,26 +19,6 @@ import cn.vove7.andro_accessibility_api.viewnode.ViewNode
*/
-fun requireBaseAccessibility(autoJump: Boolean = false) {
- AccessibilityApi.requireBaseAccessibility(autoJump)
-}
-
-suspend fun waitBaseAccessibility(waitMillis: Long = 30000) {
- AccessibilityApi.waitAccessibility(waitMillis, AccessibilityApi.BASE_SERVICE_CLS)
-}
-
-fun requireGestureAccessibility(autoJump: Boolean = false) {
- AccessibilityApi.requireGestureAccessibility(autoJump)
-}
-
-suspend fun waitGestureAccessibility(waitMillis: Long = 30000) {
- AccessibilityApi.waitAccessibility(waitMillis, AccessibilityApi.GESTURE_SERVICE_CLS)
-}
-
-suspend fun waitAccessibility(waitMillis: Long = 30000, cls: Class<*>): Boolean {
- return AccessibilityApi.waitAccessibility(waitMillis, cls)
-}
-
/**
* id 搜索
* @param id String
@@ -117,7 +96,7 @@ fun matchesText(reg: String): ConditionGroup {
* 输出布局
*/
fun printLayoutInfo(includeInvisible: Boolean = true) {
- AccessibilityApi.requireBase.rootNodeOfAllWindows.printWithChild(0, 0, includeInvisible)
+ ViewNode.getRoot().printWithChild(0, 0, includeInvisible)
}
private fun ViewNode?.printWithChild(
diff --git a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewnode/ViewInfo.kt b/core/src/main/java/cn/vove7/auto/core/viewnode/ViewInfo.kt
similarity index 92%
rename from accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewnode/ViewInfo.kt
rename to core/src/main/java/cn/vove7/auto/core/viewnode/ViewInfo.kt
index f77097e..7110d18 100644
--- a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewnode/ViewInfo.kt
+++ b/core/src/main/java/cn/vove7/auto/core/viewnode/ViewInfo.kt
@@ -1,4 +1,4 @@
-package cn.vove7.andro_accessibility_api.viewnode
+package cn.vove7.auto.core.viewnode
import android.graphics.Bitmap
import android.graphics.Rect
diff --git a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewnode/ViewNode.kt b/core/src/main/java/cn/vove7/auto/core/viewnode/ViewNode.kt
similarity index 92%
rename from accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewnode/ViewNode.kt
rename to core/src/main/java/cn/vove7/auto/core/viewnode/ViewNode.kt
index 157588d..de640de 100644
--- a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewnode/ViewNode.kt
+++ b/core/src/main/java/cn/vove7/auto/core/viewnode/ViewNode.kt
@@ -1,4 +1,4 @@
-package cn.vove7.andro_accessibility_api.viewnode
+package cn.vove7.auto.core.viewnode
import android.graphics.Point
import android.graphics.Rect
@@ -8,11 +8,10 @@ import android.view.ViewConfiguration
import android.view.accessibility.AccessibilityNodeInfo
import androidx.annotation.RequiresApi
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
-import cn.vove7.andro_accessibility_api.AccessibilityApi
-import cn.vove7.andro_accessibility_api.api.swipe
-import cn.vove7.andro_accessibility_api.utils.ScreenAdapter
-import cn.vove7.andro_accessibility_api.utils.ViewChildList
-import cn.vove7.andro_accessibility_api.viewfinder.SmartFinder
+import cn.vove7.auto.core.AutoApi
+import cn.vove7.auto.core.utils.ScreenAdapter
+import cn.vove7.auto.core.utils.ViewChildList
+import cn.vove7.auto.core.viewfinder.SmartFinder
import kotlinx.coroutines.runBlocking
import java.lang.Thread.sleep
@@ -44,16 +43,15 @@ class ViewNode(
private fun rootNodesOfAllWindows(): ViewChildList =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
ViewChildList().also { list ->
- AccessibilityApi.requireBase.windows?.forEach { win ->
+ AutoApi.windows()?.forEach { win ->
list.add(
win.root?.let { r -> ViewNode(r.also(AccessibilityNodeInfo::refresh)) }
)
-
- }
+ } ?: list.add(activeWinNode())
}
} else {
ViewChildList().also { list ->
- list.add(AccessibilityApi.requireBase.activeWinNode)
+ list.add(activeWinNode())
}
}
@@ -65,7 +63,7 @@ class ViewNode(
}
fun activeWinNode(): ViewNode? {
- return AccessibilityApi.requireBase.rootInActiveWindow?.let {
+ return AutoApi.rootInActiveWindow()?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
ViewNode(it.also(AccessibilityNodeInfo::refresh))
} else {
@@ -76,7 +74,7 @@ class ViewNode(
fun withChildren(cs: ViewChildList): ViewNode {
val root = AccessibilityNodeInfo.obtain()
- root.className = "${ROOT_TAG}[Win Size: ${cs.size}]"
+ root.className = "$ROOT_TAG[Win Size: ${cs.size}]"
return ViewNode(root).apply {
buildWithChildren = true
childrenCache = cs
@@ -112,17 +110,17 @@ class ViewNode(
@RequiresApi(Build.VERSION_CODES.N)
override fun globalClick(): Boolean {
- //获得中心点
+ // 获得中心点
val relp = ScreenAdapter.getRelPoint(getCenterPoint())
return runBlocking {
- cn.vove7.andro_accessibility_api.api.click(relp.x, relp.y)
+ cn.vove7.auto.core.api.click(relp.x, relp.y)
}
}
@RequiresApi(Build.VERSION_CODES.N)
override suspend fun globalLongClick(): Boolean {
val relp = ScreenAdapter.getRelPoint(getCenterPoint())
- return cn.vove7.andro_accessibility_api.api.longClick(relp.x, relp.y)
+ return cn.vove7.auto.core.api.longClick(relp.x, relp.y)
}
/**
@@ -163,7 +161,7 @@ class ViewNode(
get() = if (buildWithChildren) childrenCache?.size ?: 0 else node.childCount
override fun childAt(i: Int): ViewNode? {
- //IllegalStateException: Cannot perform this action on a not sealed instance.
+ // IllegalStateException: Cannot perform this action on a not sealed instance.
if (buildWithChildren) {
return childrenCache?.get(i)
}
@@ -314,7 +312,7 @@ class ViewNode(
override fun swipeOffset(dx: Int, dy: Int, delay: Int): Boolean {
val c = ScreenAdapter.getRelPoint(getCenterPoint())
return runBlocking {
- swipe(c.x, c.y, c.x + dx, c.y + dy, delay)
+ cn.vove7.auto.core.api.swipe(c.x, c.y, c.x + dx, c.y + dy, delay)
}
}
diff --git a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewnode/ViewOperation.kt b/core/src/main/java/cn/vove7/auto/core/viewnode/ViewOperation.kt
similarity index 95%
rename from accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewnode/ViewOperation.kt
rename to core/src/main/java/cn/vove7/auto/core/viewnode/ViewOperation.kt
index 4798b01..5e21eca 100644
--- a/accessibility-api/src/main/java/cn/vove7/andro_accessibility_api/viewnode/ViewOperation.kt
+++ b/core/src/main/java/cn/vove7/auto/core/viewnode/ViewOperation.kt
@@ -1,11 +1,13 @@
-package cn.vove7.andro_accessibility_api.viewnode
+package cn.vove7.auto.core.viewnode
import android.graphics.Point
import android.graphics.Rect
+import android.os.Build
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeInfo.RangeInfo
-import cn.vove7.andro_accessibility_api.utils.ViewChildList
-import cn.vove7.andro_accessibility_api.viewfinder.SmartFinder
+import androidx.annotation.RequiresApi
+import cn.vove7.auto.core.utils.ViewChildList
+import cn.vove7.auto.core.viewfinder.SmartFinder
/**
* ViewNode 操作方法集合
diff --git a/settings.gradle b/settings.gradle
index 1c46029..0b22b53 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,5 @@
include ':accessibility-api'
include ':app'
-rootProject.name = "Android-Accessibility-Api"
\ No newline at end of file
+rootProject.name = "Android-Accessibility-Api"
+include ':core'
+include ':uiauto'
diff --git a/uiauto/.gitignore b/uiauto/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/uiauto/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/uiauto/build.gradle b/uiauto/build.gradle
new file mode 100644
index 0000000..95dfc73
--- /dev/null
+++ b/uiauto/build.gradle
@@ -0,0 +1,39 @@
+plugins {
+ id 'com.android.library'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ namespace 'cn.vove7.accessibility.uiauto'
+ compileSdk 33
+
+ defaultConfig {
+ minSdk 21
+ targetSdk 33
+
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+dependencies {
+ implementation 'androidx.core:core-ktx:1.10.0'
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ api 'androidx.test.uiautomator:uiautomator:2.2.0'
+ implementation 'com.jakewharton.timber:timber:5.0.1'
+
+ api project(":core")
+}
\ No newline at end of file
diff --git a/uiauto/consumer-rules.pro b/uiauto/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/uiauto/proguard-rules.pro b/uiauto/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/uiauto/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/uiauto/src/main/AndroidManifest.xml b/uiauto/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5b0baea
--- /dev/null
+++ b/uiauto/src/main/AndroidManifest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/uiauto/src/main/java/cn/vove7/accessibility/uiauto/AutoInstrumentation.kt b/uiauto/src/main/java/cn/vove7/accessibility/uiauto/AutoInstrumentation.kt
new file mode 100644
index 0000000..27229ab
--- /dev/null
+++ b/uiauto/src/main/java/cn/vove7/accessibility/uiauto/AutoInstrumentation.kt
@@ -0,0 +1,173 @@
+package cn.vove7.accessibility.uiauto
+
+import android.accessibilityservice.AccessibilityServiceInfo
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.Instrumentation
+import android.app.UiAutomation
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.hardware.display.DisplayManager
+import android.os.*
+import android.util.SparseArray
+import android.view.*
+import android.view.accessibility.AccessibilityEvent.TYPE_WINDOWS_CHANGED
+import android.view.accessibility.AccessibilityNodeInfo
+import android.view.accessibility.AccessibilityWindowInfo
+import androidx.annotation.CallSuper
+import androidx.annotation.Keep
+import androidx.annotation.RequiresApi
+import cn.vove7.auto.core.AppScope
+import cn.vove7.auto.core.AutoApi
+import cn.vove7.auto.core.OnPageUpdate
+import cn.vove7.auto.core.PageUpdateMonitor
+import cn.vove7.auto.core.utils.AutoGestureDescription
+import cn.vove7.auto.core.utils.GestureResultCallback
+import kotlinx.coroutines.*
+import timber.log.Timber
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * # AutoInstrumentation
+ *
+ * @author Libra
+ * @date 2023/4/25
+ */
+@Suppress("unused")
+@Keep
+open class AutoInstrumentation : Instrumentation(), AutoApi {
+
+ open val enableListenPageUpdate: Boolean = true
+ private val pageListener: OnPageUpdate = ::onPageUpdate
+
+ companion object {
+ var INS: Instrumentation? = null
+ val uiAutomation: UiAutomation? get() = INS?.uiAutomation
+ }
+
+ private var gestureSeq = AtomicInteger(0)
+ private val motionEventInjector by lazy {
+ MotionInjector(Looper.getMainLooper(), uiAutomation)
+ }
+
+ override fun onCreate(arguments: Bundle?) {
+ if (Timber.treeCount == 0) {
+ Timber.plant(Timber.DebugTree())
+ }
+ Timber.i("AutoInstrumentation onCreate.")
+ INS = this
+ start()
+ }
+
+ private fun initPageListener() {
+ PageUpdateMonitor.enableListenPageUpdate = enableListenPageUpdate
+ PageUpdateMonitor.addOnPageUpdateListener(pageListener)
+ uiAutomation.setOnAccessibilityEventListener(PageUpdateMonitor)
+ }
+
+ private fun initServiceInfo() {
+ val si = uiAutomation.serviceInfo
+ si.eventTypes = TYPE_WINDOWS_CHANGED
+ si.flags = si.flags or AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS or
+ AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY
+ si.feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK
+ si.packageNames = null
+ Timber.i("uiAutomation.serviceInfo: $si")
+ uiAutomation.serviceInfo = si
+ }
+
+ @CallSuper
+ override fun onStart() {
+ registerImpl()
+ initPageListener()
+ initServiceInfo()
+
+ Timber.i("AutoInstrumentation started.")
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ Timber.i("windowsOnAllDisplays: ${uiAutomation.windowsOnAllDisplays}")
+ }
+ Timber.i("windows: ${uiAutomation.windows}")
+
+ }
+
+ open fun onPageUpdate(currentScope: AppScope) {}
+
+
+ override fun rootInActiveWindow(): AccessibilityNodeInfo? = uiAutomation.rootInActiveWindow
+
+ override fun windows(): List? = uiAutomation.windows
+
+ @RequiresApi(Build.VERSION_CODES.R)
+ override fun windowsOnAllDisplays(): SparseArray> {
+ return uiAutomation.windowsOnAllDisplays
+ }
+
+ override fun performAction(action: Int) = uiAutomation.performGlobalAction(action)
+
+ override suspend fun doGesturesAsync(
+ gesture: AutoGestureDescription,
+ callback: GestureResultCallback?,
+ handler: Handler?
+ ) {
+ val displayId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) gesture.displayId else 0
+ val sampleTimeMs = calculateGestureSampleTimeMs(displayId)
+ val steps = AutoGestureDescription.MotionEventGenerator.getGestureStepsFromGestureDescription(gesture, sampleTimeMs)
+ val seq = gestureSeq.getAndAdd(1)
+ motionEventInjector.injectEvents(steps, seq, displayId) { _, success ->
+ if (success) {
+ callback?.onCompleted(gesture)
+ } else {
+ callback?.onCancelled(gesture)
+ }
+ }
+ }
+
+ override fun injectInputEvent(event: InputEvent, sync: Boolean) {
+ uiAutomation.injectInputEvent(event, sync)
+ }
+
+ override fun sendString(text: String) = sendStringSync(text)
+
+
+ override fun sendKeyCode(keyCode: Int): Boolean {
+ val eventTime = SystemClock.uptimeMillis()
+ val downEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
+ keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
+ InputDevice.SOURCE_KEYBOARD)
+ if (uiAutomation.injectInputEvent(downEvent, false)) {
+ val upEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
+ keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
+ InputDevice.SOURCE_KEYBOARD)
+ if (uiAutomation.injectInputEvent(upEvent, false)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ override fun takeScreenshot(): Bitmap? {
+ return uiAutomation.takeScreenshot()
+ }
+
+ private fun calculateGestureSampleTimeMs(displayId: Int): Int {
+ val display: Display = (context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager).getDisplay(
+ displayId) ?: return 100
+ val msPerSecond = 1000
+ val sampleTimeMs = (msPerSecond / display.refreshRate).toInt()
+ return if (sampleTimeMs < 1) {
+ // Should be impossible, but do not return 0.
+ 100
+ } else sampleTimeMs
+ }
+
+ override fun onDestroy() {
+ Timber.i("AutoInstrumentation destroyed.")
+ motionEventInjector.onDestroy()
+ INS = null
+ PageUpdateMonitor.removeOnPageUpdateListener(pageListener)
+ destroyAutoService()
+ }
+
+}
diff --git a/uiauto/src/main/java/cn/vove7/accessibility/uiauto/GestureCallback.kt b/uiauto/src/main/java/cn/vove7/accessibility/uiauto/GestureCallback.kt
new file mode 100644
index 0000000..42b0e8f
--- /dev/null
+++ b/uiauto/src/main/java/cn/vove7/accessibility/uiauto/GestureCallback.kt
@@ -0,0 +1,14 @@
+package cn.vove7.accessibility.uiauto
+
+import android.os.RemoteException
+
+/**
+ * # GestureCallback
+ *
+ * @author Libra
+ * @date 2023/4/26
+ */
+fun interface GestureCallback {
+
+ fun onPerformGestureResult(sequence: Int, completedSuccessfully: Boolean)
+}
\ No newline at end of file
diff --git a/uiauto/src/main/java/cn/vove7/accessibility/uiauto/MotionInjector.java b/uiauto/src/main/java/cn/vove7/accessibility/uiauto/MotionInjector.java
new file mode 100644
index 0000000..f23c0ff
--- /dev/null
+++ b/uiauto/src/main/java/cn/vove7/accessibility/uiauto/MotionInjector.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright (C) 2015 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 cn.vove7.accessibility.uiauto;
+
+import android.app.UiAutomation;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.MotionEvent;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import cn.vove7.auto.core.utils.AutoGestureDescription;
+import timber.log.Timber;
+
+/**
+ * Injects MotionEvents to permit {@code AccessibilityService}s to touch the screen on behalf of
+ * users.
+ *
+ * All methods except {@code injectEvents} must be called only from the main thread.
+ */
+public class MotionInjector implements Handler.Callback {
+ private static final String LOG_TAG = "MotionInjector";
+ private static final int MESSAGE_SEND_MOTION_EVENT = 1;
+ private static final int MESSAGE_INJECT_EVENTS = 2;
+
+ /**
+ * Constants used to initialize all MotionEvents
+ */
+ private static final int EVENT_META_STATE = 0;
+ private static final int EVENT_BUTTON_STATE = 0;
+ private static final int EVENT_EDGE_FLAGS = 0;
+ private static final int EVENT_SOURCE = InputDevice.SOURCE_TOUCHSCREEN;
+ private static final int EVENT_FLAGS = 0;
+ private static final float EVENT_X_PRECISION = 1;
+ private static final float EVENT_Y_PRECISION = 1;
+
+ private static MotionEvent.PointerCoords[] sPointerCoords;
+ private static MotionEvent.PointerProperties[] sPointerProps;
+
+ private final Handler mHandler;
+ private final SparseArray mOpenGesturesInProgress = new SparseArray<>();
+
+ private GestureCallback mServiceInterfaceForCurrentGesture;
+ private List mSequencesInProgress = new ArrayList<>(5);
+ private boolean mIsDestroyed = false;
+ private AutoGestureDescription.TouchPoint[] mLastTouchPoints;
+ private int mNumLastTouchPoints;
+ private long mDownTime;
+ private long mLastScheduledEventTime;
+ private SparseIntArray mStrokeIdToPointerId = new SparseIntArray(5);
+
+ UiAutomation uiAutomation;
+
+ /**
+ * @param looper A looper on the main thread to use for dispatching new events
+ */
+ public MotionInjector(Looper looper, UiAutomation uiAutomation) {
+ mHandler = new Handler(looper, this);
+ this.uiAutomation = uiAutomation;
+ }
+
+ /**
+ * @param handler A handler to post messages. Exposes internal state for testing only.
+ */
+ public MotionInjector(Handler handler, UiAutomation uiAutomation) {
+ mHandler = handler;
+ this.uiAutomation = uiAutomation;
+ }
+
+ /**
+ * Schedule a gesture for injection. The gesture is defined by a set of {@code GestureStep}s,
+ * from which {@code MotionEvent}s will be derived. All gestures currently in progress will be
+ * cancelled.
+ *
+ * @param gestureSteps The gesture steps to inject.
+ * either complete or cancelled.
+ */
+ public void injectEvents(
+ List gestureSteps,
+ int sequence, int displayId,
+ GestureCallback callback
+ ) {
+ Object[] args = new Object[]{
+ gestureSteps,
+ callback,
+ sequence,
+ displayId
+ };
+ mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_INJECT_EVENTS, args));
+ }
+
+ public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ // MotionEventInjector would cancel any injected gesture when any MotionEvent arrives.
+ // For user using an external device to control the pointer movement, it's almost
+ // impossible to perform the gestures. Any slightly unintended movement results in the
+ // cancellation of the gesture.
+ if ((event.isFromSource(InputDevice.SOURCE_MOUSE)
+ && event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE)
+ && mOpenGesturesInProgress.get(EVENT_SOURCE, false)) {
+ return;
+ }
+ cancelAnyPendingInjectedEvents();
+ // Indicate that the input event is injected from accessibility, to let applications
+ // distinguish it from events injected by other means.
+ policyFlags |= FLAG_INJECTED_FROM_ACCESSIBILITY;
+ sendMotionEventToNext(event, rawEvent, policyFlags);
+ }
+
+ public void clearEvents(int inputSource) {
+ /*
+ * Reset state for motion events passing through so we won't send a cancel event for
+ * them.
+ */
+ if (!mHandler.hasMessages(MESSAGE_SEND_MOTION_EVENT)) {
+ mOpenGesturesInProgress.put(inputSource, false);
+ }
+ }
+
+ public void onDestroy() {
+ cancelAnyPendingInjectedEvents();
+ mIsDestroyed = true;
+ }
+
+ @Override
+ public boolean handleMessage(Message message) {
+ if (message.what == MESSAGE_INJECT_EVENTS) {
+ Object[] args = (Object[]) message.obj;
+ injectEventsMainThread((List) args[0],
+ (GestureCallback) args[1], (int) args[2], (int) args[3]);
+ return true;
+ }
+ if (message.what != MESSAGE_SEND_MOTION_EVENT) {
+ Timber.tag(LOG_TAG).e("Unknown message: %s", message.what);
+ return false;
+ }
+ MotionEvent motionEvent = (MotionEvent) message.obj;
+ sendMotionEventToNext(motionEvent, motionEvent,
+ FLAG_PASS_TO_USER
+ | FLAG_INJECTED_FROM_ACCESSIBILITY);
+ boolean isEndOfSequence = message.arg1 != 0;
+ if (isEndOfSequence) {
+ notifyService(mServiceInterfaceForCurrentGesture, mSequencesInProgress.get(0), true);
+ mSequencesInProgress.remove(0);
+ }
+ return true;
+ }
+
+ private Object getNext() {
+ return this;
+ }
+
+ private void injectEventsMainThread(List gestureSteps,
+ GestureCallback serviceInterface,
+ int sequence, int displayId) {
+ if (mIsDestroyed) {
+ serviceInterface.onPerformGestureResult(sequence, false);
+ return;
+ }
+
+ if (getNext() == null) {
+ notifyService(serviceInterface, sequence, false);
+ return;
+ }
+
+ boolean continuingGesture = newGestureTriesToContinueOldOne(gestureSteps);
+
+ if (continuingGesture) {
+ if (/*(serviceInterface != mServiceInterfaceForCurrentGesture)
+ || */!prepareToContinueOldGesture(gestureSteps)) {
+ cancelAnyPendingInjectedEvents();
+ notifyService(serviceInterface, sequence, false);
+ return;
+ }
+ }
+ if (!continuingGesture) {
+ cancelAnyPendingInjectedEvents();
+ // Injected gestures have been canceled, but real gestures still need cancelling
+ cancelAnyGestureInProgress(EVENT_SOURCE);
+ }
+ mServiceInterfaceForCurrentGesture = serviceInterface;
+
+ long currentTime = SystemClock.uptimeMillis();
+ List events = getMotionEventsFromGestureSteps(gestureSteps,
+ (mSequencesInProgress.size() == 0) ? currentTime : mLastScheduledEventTime);
+ if (events.isEmpty()) {
+ notifyService(serviceInterface, sequence, false);
+ return;
+ }
+ mSequencesInProgress.add(sequence);
+
+ for (int i = 0; i < events.size(); i++) {
+ MotionEvent event = events.get(i);
+ // event.setDisplayId(displayId);
+ int isEndOfSequence = (i == events.size() - 1) ? 1 : 0;
+ Message message = mHandler.obtainMessage(
+ MESSAGE_SEND_MOTION_EVENT, isEndOfSequence, 0, event);
+ mLastScheduledEventTime = event.getEventTime();
+ mHandler.sendMessageDelayed(message, Math.max(0, event.getEventTime() - currentTime));
+ }
+ }
+
+ private boolean newGestureTriesToContinueOldOne(List gestureSteps) {
+ if (gestureSteps.isEmpty()) {
+ return false;
+ }
+ AutoGestureDescription.GestureStep firstStep = gestureSteps.get(0);
+ for (int i = 0; i < firstStep.numTouchPoints; i++) {
+ if (!firstStep.touchPoints[i].mIsStartOfPath) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * A gesture can only continue a gesture if it contains intermediate points that continue
+ * each continued stroke of the last gesture, and no extra points.
+ *
+ * @param gestureSteps The steps of the new gesture
+ * @return {@code true} if the new gesture could continue the last one dispatched. {@code false}
+ * otherwise.
+ */
+ private boolean prepareToContinueOldGesture(List gestureSteps) {
+ if (gestureSteps.isEmpty() || (mLastTouchPoints == null) || (mNumLastTouchPoints == 0)) {
+ return false;
+ }
+ AutoGestureDescription.GestureStep firstStep = gestureSteps.get(0);
+ // Make sure all of the continuing paths match up
+ int numContinuedStrokes = 0;
+ for (int i = 0; i < firstStep.numTouchPoints; i++) {
+ AutoGestureDescription.TouchPoint touchPoint = firstStep.touchPoints[i];
+ if (!touchPoint.mIsStartOfPath) {
+ int continuedPointerId = mStrokeIdToPointerId
+ .get(touchPoint.mContinuedStrokeId, -1);
+ if (continuedPointerId == -1) {
+ Timber.tag(LOG_TAG).w("Can't continue gesture due to unknown continued stroke id in %s", touchPoint);
+ return false;
+ }
+ mStrokeIdToPointerId.put(touchPoint.mStrokeId, continuedPointerId);
+ int lastPointIndex = findPointByStrokeId(
+ mLastTouchPoints, mNumLastTouchPoints, touchPoint.mContinuedStrokeId);
+ if (lastPointIndex < 0) {
+ Timber.tag(LOG_TAG).w("Can't continue gesture due continued gesture id of "
+ + touchPoint + " not matching any previous strokes in "
+ + Arrays.asList(mLastTouchPoints));
+ return false;
+ }
+ if (mLastTouchPoints[lastPointIndex].mIsEndOfPath
+ || (mLastTouchPoints[lastPointIndex].mX != touchPoint.mX)
+ || (mLastTouchPoints[lastPointIndex].mY != touchPoint.mY)) {
+ Timber.tag(LOG_TAG).w("Can't continue gesture due to points mismatch between "
+ + mLastTouchPoints[lastPointIndex] + " and " + touchPoint);
+ return false;
+ }
+ // Update the last touch point to match the continuation, so the gestures will
+ // line up
+ mLastTouchPoints[lastPointIndex].mStrokeId = touchPoint.mStrokeId;
+ }
+ numContinuedStrokes++;
+ }
+ // Make sure we didn't miss any paths
+ for (int i = 0; i < mNumLastTouchPoints; i++) {
+ if (!mLastTouchPoints[i].mIsEndOfPath) {
+ numContinuedStrokes--;
+ }
+ }
+ return numContinuedStrokes == 0;
+ }
+
+ private void sendMotionEventToNext(MotionEvent event, MotionEvent rawEvent,
+ int policyFlags) {
+ if (getNext() != null) {
+ // super.onMotionEvent(event, rawEvent, policyFlags);
+ uiAutomation.injectInputEvent(event, false);
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mOpenGesturesInProgress.put(event.getSource(), true);
+ }
+ if ((event.getActionMasked() == MotionEvent.ACTION_UP)
+ || (event.getActionMasked() == MotionEvent.ACTION_CANCEL)) {
+ mOpenGesturesInProgress.put(event.getSource(), false);
+ }
+ }
+ }
+
+ int FLAG_PASS_TO_USER = 0x40000000;
+
+ public static final int POLICY_FLAG_INJECTED_FROM_ACCESSIBILITY = 131072;
+ int FLAG_INJECTED_FROM_ACCESSIBILITY = POLICY_FLAG_INJECTED_FROM_ACCESSIBILITY;
+
+ private void cancelAnyGestureInProgress(int source) {
+ if ((getNext() != null) && mOpenGesturesInProgress.get(source, false)) {
+ long now = SystemClock.uptimeMillis();
+ MotionEvent cancelEvent =
+ obtainMotionEvent(now, now, MotionEvent.ACTION_CANCEL, getLastTouchPoints(), 1);
+ sendMotionEventToNext(cancelEvent, cancelEvent,
+ FLAG_PASS_TO_USER
+ | FLAG_INJECTED_FROM_ACCESSIBILITY);
+ mOpenGesturesInProgress.put(source, false);
+ }
+ }
+
+ private void cancelAnyPendingInjectedEvents() {
+ if (mHandler.hasMessages(MESSAGE_SEND_MOTION_EVENT)) {
+ mHandler.removeMessages(MESSAGE_SEND_MOTION_EVENT);
+ cancelAnyGestureInProgress(EVENT_SOURCE);
+ for (int i = mSequencesInProgress.size() - 1; i >= 0; i--) {
+ notifyService(mServiceInterfaceForCurrentGesture,
+ mSequencesInProgress.get(i), false);
+ mSequencesInProgress.remove(i);
+ }
+ } else if (mNumLastTouchPoints != 0) {
+ // An injected gesture is in progress and waiting for a continuation. Cancel it.
+ cancelAnyGestureInProgress(EVENT_SOURCE);
+ }
+ mNumLastTouchPoints = 0;
+ mStrokeIdToPointerId.clear();
+ }
+
+ private void notifyService(GestureCallback callback, int sequence, boolean success) {
+ callback.onPerformGestureResult(sequence, success);
+ }
+
+ private List getMotionEventsFromGestureSteps(
+ List steps, long startTime) {
+ final List motionEvents = new ArrayList<>();
+
+ AutoGestureDescription.TouchPoint[] lastTouchPoints = getLastTouchPoints();
+
+ for (int i = 0; i < steps.size(); i++) {
+ AutoGestureDescription.GestureStep step = steps.get(i);
+ int currentTouchPointSize = step.numTouchPoints;
+ if (currentTouchPointSize > lastTouchPoints.length) {
+ mNumLastTouchPoints = 0;
+ motionEvents.clear();
+ return motionEvents;
+ }
+
+ appendMoveEventIfNeeded(motionEvents, step.touchPoints, currentTouchPointSize,
+ startTime + step.timeSinceGestureStart);
+ appendUpEvents(motionEvents, step.touchPoints, currentTouchPointSize,
+ startTime + step.timeSinceGestureStart);
+ appendDownEvents(motionEvents, step.touchPoints, currentTouchPointSize,
+ startTime + step.timeSinceGestureStart);
+ }
+ return motionEvents;
+ }
+
+ private AutoGestureDescription.TouchPoint[] getLastTouchPoints() {
+ if (mLastTouchPoints == null) {
+ int capacity = 20;// GestureDescription.getMaxStrokeCount();
+ mLastTouchPoints = new AutoGestureDescription.TouchPoint[capacity];
+ for (int i = 0; i < capacity; i++) {
+ mLastTouchPoints[i] = new AutoGestureDescription.TouchPoint();
+ }
+ }
+ return mLastTouchPoints;
+ }
+
+ private void appendMoveEventIfNeeded(List motionEvents,
+ AutoGestureDescription.TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
+ /* Look for pointers that have moved */
+ boolean moveFound = false;
+ AutoGestureDescription.TouchPoint[] lastTouchPoints = getLastTouchPoints();
+ for (int i = 0; i < currentTouchPointsSize; i++) {
+ int lastPointsIndex = findPointByStrokeId(lastTouchPoints, mNumLastTouchPoints,
+ currentTouchPoints[i].mStrokeId);
+ if (lastPointsIndex >= 0) {
+ moveFound |= (lastTouchPoints[lastPointsIndex].mX != currentTouchPoints[i].mX)
+ || (lastTouchPoints[lastPointsIndex].mY != currentTouchPoints[i].mY);
+ lastTouchPoints[lastPointsIndex].copyFrom(currentTouchPoints[i]);
+ }
+ }
+
+ if (moveFound) {
+ motionEvents.add(obtainMotionEvent(mDownTime, currentTime, MotionEvent.ACTION_MOVE,
+ lastTouchPoints, mNumLastTouchPoints));
+ }
+ }
+
+ private void appendUpEvents(List motionEvents,
+ AutoGestureDescription.TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
+ /* Look for a pointer at the end of its path */
+ AutoGestureDescription.TouchPoint[] lastTouchPoints = getLastTouchPoints();
+ for (int i = 0; i < currentTouchPointsSize; i++) {
+ if (currentTouchPoints[i].mIsEndOfPath) {
+ int indexOfUpEvent = findPointByStrokeId(lastTouchPoints, mNumLastTouchPoints,
+ currentTouchPoints[i].mStrokeId);
+ if (indexOfUpEvent < 0) {
+ continue; // Should not happen
+ }
+ int action = (mNumLastTouchPoints == 1) ? MotionEvent.ACTION_UP
+ : MotionEvent.ACTION_POINTER_UP;
+ action |= indexOfUpEvent << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+ motionEvents.add(obtainMotionEvent(mDownTime, currentTime, action,
+ lastTouchPoints, mNumLastTouchPoints));
+ /* Remove this point from lastTouchPoints */
+ for (int j = indexOfUpEvent; j < mNumLastTouchPoints - 1; j++) {
+ lastTouchPoints[j].copyFrom(mLastTouchPoints[j + 1]);
+ }
+ mNumLastTouchPoints--;
+ if (mNumLastTouchPoints == 0) {
+ mStrokeIdToPointerId.clear();
+ }
+ }
+ }
+ }
+
+ private void appendDownEvents(List motionEvents,
+ AutoGestureDescription.TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
+ /* Look for a pointer that is just starting */
+ AutoGestureDescription.TouchPoint[] lastTouchPoints = getLastTouchPoints();
+ for (int i = 0; i < currentTouchPointsSize; i++) {
+ if (currentTouchPoints[i].mIsStartOfPath) {
+ /* Add the point to last coords and use the new array to generate the event */
+ lastTouchPoints[mNumLastTouchPoints++].copyFrom(currentTouchPoints[i]);
+ int action = (mNumLastTouchPoints == 1) ? MotionEvent.ACTION_DOWN
+ : MotionEvent.ACTION_POINTER_DOWN;
+ if (action == MotionEvent.ACTION_DOWN) {
+ mDownTime = currentTime;
+ }
+ action |= i << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+ motionEvents.add(obtainMotionEvent(mDownTime, currentTime, action,
+ lastTouchPoints, mNumLastTouchPoints));
+ }
+ }
+ }
+
+ private MotionEvent obtainMotionEvent(long downTime, long eventTime, int action,
+ AutoGestureDescription.TouchPoint[] touchPoints, int touchPointsSize) {
+ if ((sPointerCoords == null) || (sPointerCoords.length < touchPointsSize)) {
+ sPointerCoords = new MotionEvent.PointerCoords[touchPointsSize];
+ for (int i = 0; i < touchPointsSize; i++) {
+ sPointerCoords[i] = new MotionEvent.PointerCoords();
+ }
+ }
+ if ((sPointerProps == null) || (sPointerProps.length < touchPointsSize)) {
+ sPointerProps = new MotionEvent.PointerProperties[touchPointsSize];
+ for (int i = 0; i < touchPointsSize; i++) {
+ sPointerProps[i] = new MotionEvent.PointerProperties();
+ }
+ }
+ for (int i = 0; i < touchPointsSize; i++) {
+ int pointerId = mStrokeIdToPointerId.get(touchPoints[i].mStrokeId, -1);
+ if (pointerId == -1) {
+ pointerId = getUnusedPointerId();
+ mStrokeIdToPointerId.put(touchPoints[i].mStrokeId, pointerId);
+ }
+ sPointerProps[i].id = pointerId;
+ sPointerProps[i].toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+ sPointerCoords[i].clear();
+ sPointerCoords[i].pressure = 1.0f;
+ sPointerCoords[i].size = 1.0f;
+ sPointerCoords[i].x = touchPoints[i].mX;
+ sPointerCoords[i].y = touchPoints[i].mY;
+ }
+ return MotionEvent.obtain(downTime, eventTime, action, touchPointsSize,
+ sPointerProps, sPointerCoords, EVENT_META_STATE, EVENT_BUTTON_STATE,
+ EVENT_X_PRECISION, EVENT_Y_PRECISION, KeyCharacterMap.VIRTUAL_KEYBOARD,
+ EVENT_EDGE_FLAGS, EVENT_SOURCE, EVENT_FLAGS);
+ }
+
+ private static int findPointByStrokeId(AutoGestureDescription.TouchPoint[] touchPoints, int touchPointsSize,
+ int strokeId) {
+ for (int i = 0; i < touchPointsSize; i++) {
+ if (touchPoints[i].mStrokeId == strokeId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private int getUnusedPointerId() {
+ int MAX_POINTER_ID = 10;
+ int pointerId = 0;
+ while (mStrokeIdToPointerId.indexOfValue(pointerId) >= 0) {
+ pointerId++;
+ if (pointerId >= MAX_POINTER_ID) {
+ return MAX_POINTER_ID;
+ }
+ }
+ return pointerId;
+ }
+}
\ No newline at end of file